35 | // }
36 | // }
37 | // }
38 |
--------------------------------------------------------------------------------
/frontend/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import "./commands";
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | NEETBOX
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "neetcenter",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev:vite": "vite dev",
8 | "dev": "rsbuild dev",
9 | "build:vite": "vite build",
10 | "build": "rsbuild build",
11 | "tsc": "tsc",
12 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 20",
13 | "preview": "vite preview",
14 | "prettier-check": "prettier -c . !.yarn",
15 | "prettier": "prettier -w . !.yarn",
16 | "e2e": "cypress run --headed",
17 | "e2e:ci": "cypress run"
18 | },
19 | "dependencies": {
20 | "@douyinfe/semi-icons": "^2.78.0",
21 | "@douyinfe/semi-ui": "^2.78.0",
22 | "@semi-bot/semi-theme-neetbox": "^1.0.7",
23 | "@uiw/react-json-view": "^2.0.0-alpha.30",
24 | "@uiw/react-md-editor": "^4.0.5",
25 | "echarts": "^5.6.0",
26 | "jotai": "^2.12.2",
27 | "katex": "^0.16.21",
28 | "mermaid": "^11.6.0",
29 | "nanoid": "^5.1.5",
30 | "react": "^18.3.1",
31 | "react-dom": "^18.3.1",
32 | "react-router-dom": "^6.30.0",
33 | "rehype-katex": "^7.0.1",
34 | "remark-gfm": "^4.0.1",
35 | "remark-math": "^6.0.0",
36 | "styled-components": "^6.1.16",
37 | "swr": "^2.3.3",
38 | "vite-plugin-semi-theme": "^0.6.0"
39 | },
40 | "devDependencies": {
41 | "@douyinfe/semi-rspack-plugin": "^2.78.0",
42 | "@rsbuild/core": "^0.5.9",
43 | "@rsbuild/plugin-react": "^0.1.9",
44 | "@types/react": "^18.3.20",
45 | "@types/react-dom": "^18.3.5",
46 | "@typescript-eslint/eslint-plugin": "^6.21.0",
47 | "@typescript-eslint/parser": "^6.21.0",
48 | "@vitejs/plugin-react": "^4.3.4",
49 | "cypress": "^13.17.0",
50 | "eslint": "^8.57.1",
51 | "eslint-import-resolver-typescript": "^3.9.1",
52 | "eslint-plugin-import": "^2.31.0",
53 | "eslint-plugin-react-hooks": "^5.2.0",
54 | "eslint-plugin-react-refresh": "^0.4.19",
55 | "prettier": "^3.5.3",
56 | "typescript": "^5.8.2",
57 | "vite": "^6.3.4"
58 | },
59 | "prettier": {
60 | "trailingComma": "all",
61 | "printWidth": 110
62 | },
63 | "packageManager": "yarn@4.5.0"
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-Bold.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-BoldItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-ExtraBold.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-ExtraBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-ExtraBoldItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-ExtraLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-ExtraLight.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-ExtraLightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-ExtraLightItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-Italic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-Light.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-LightItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-Medium.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-MediumItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-Regular.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-SemiBold.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-Thin.woff2
--------------------------------------------------------------------------------
/frontend/public/fonts/maple/MapleMono-ThinItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/visualDust/neetbox/38a44edd98fcd35097406fae7b5d3ade0ae43b4d/frontend/public/fonts/maple/MapleMono-ThinItalic.woff2
--------------------------------------------------------------------------------
/frontend/public/logo.old.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/rsbuild.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@rsbuild/core";
2 | import { pluginReact } from "@rsbuild/plugin-react";
3 | import { SemiRspackPlugin } from "@douyinfe/semi-rspack-plugin";
4 |
5 | const server = new URL("http://127.0.0.1:20202");
6 |
7 | export default defineConfig({
8 | plugins: [pluginReact()],
9 | source: {
10 | alias: {
11 | "@": "./src",
12 | },
13 | entry: { index: "./src/main.tsx" },
14 | },
15 | tools: {
16 | rspack: (config) => {
17 | config.plugins!.push(
18 | new SemiRspackPlugin({
19 | theme: "@semi-bot/semi-theme-neetbox",
20 | }),
21 | );
22 | // config.optimization = { ...config.optimization, minimize: false };
23 | config.module = {
24 | ...config.module,
25 | rules: [
26 | ...(config.module?.rules ?? []),
27 | {
28 | test: /echarts/,
29 | sideEffects: true,
30 | },
31 | ],
32 | };
33 | },
34 | },
35 | html: {
36 | title: "NEETBOX",
37 | favicon: "./public/logo.svg",
38 | },
39 | performance: {
40 | chunkSplit: { strategy: "all-in-one" },
41 | },
42 | server: {
43 | port: 5173,
44 | proxy: {
45 | "/api/": {
46 | target: server.href,
47 | },
48 | "/ws/project/": {
49 | target: `ws://${server.host}`,
50 | },
51 | },
52 | },
53 | // dev: {
54 | // writeToDisk: true,
55 | // },
56 | output: {
57 | assetPrefix: "/web/",
58 | distPath: {
59 | root: "../neetbox/frontend_dist",
60 | },
61 | },
62 | });
63 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "@douyinfe/semi-ui";
2 | import { Outlet } from "react-router-dom";
3 | import { useReportGlobalError } from "./hooks/useReportError";
4 | import AppHeader from "./components/layout/AppHeader";
5 | import "./styles/global.css";
6 |
7 | export default function App() {
8 | useReportGlobalError();
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/components/appTitle.tsx:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai";
2 | import { PropsWithChildren, ReactNode, useEffect } from "react";
3 |
4 | const appTitle = atom([{ title: "" }] as Array<{ title: ReactNode; extra?: ReactNode }>);
5 |
6 | export const useTitle = () => useAtom(appTitle)[0].at(-1)!;
7 |
8 | export const AppTitle = (props: PropsWithChildren<{ extra?: ReactNode }>) => {
9 | const [_, setTitle] = useAtom(appTitle);
10 | useEffect(() => {
11 | setTitle((arr) => [...arr, { title: props.children, extra: props.extra }]);
12 | return () => setTitle((arr) => arr.slice(0, -1));
13 | }, [props.children, props.extra, setTitle]);
14 | return null;
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/src/components/common/centerBox.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, PropsWithChildren } from "react";
2 |
3 | export const CenterBox = (props: PropsWithChildren<{ style?: CSSProperties }>) => {
4 | return (
5 |
6 | {props.children}
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/frontend/src/components/common/errorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { useRouteError } from "react-router";
3 |
4 | export const RouteError = () => {
5 | const routeError = useRouteError() as { error: Error };
6 | const error = routeError?.error ?? routeError;
7 | const text = `RouteError\n\n${error}\n\n${error?.stack}`;
8 | return {text}
;
9 | };
10 |
11 | export class ErrorBoundary extends React.Component<
12 | React.PropsWithChildren<{ renderError?: (error: Error, errorInfo: React.ErrorInfo) => ReactNode }>
13 | > {
14 | state: { error?: Error; errorInfo?: React.ErrorInfo } = {};
15 |
16 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
17 | this.setState({ error, errorInfo });
18 | }
19 | render(): React.ReactNode {
20 | return this.state.error
21 | ? (this.props.renderError?.(this.state.error, this.state.errorInfo!) ?? this.renderDefaultErrorPage())
22 | : this.props.children;
23 | }
24 |
25 | renderDefaultErrorPage() {
26 | console.info(this.state);
27 | const text = `${this.state.error}\n\n${(this.state.error as Error)?.stack}\n\n${
28 | this.state.errorInfo?.componentStack
29 | }`;
30 | return {text}
;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/components/common/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Space, Spin } from "@douyinfe/semi-ui";
2 | import { SpinSize } from "@douyinfe/semi-ui/lib/es/spin";
3 | import { ReactNode } from "react";
4 |
5 | export default function Loading({
6 | width = "",
7 | height = "100px",
8 | size = "middle",
9 | text,
10 | vertical,
11 | }: {
12 | width?: string;
13 | height?: string;
14 | size?: SpinSize;
15 | text?: ReactNode;
16 | vertical?: boolean;
17 | }) {
18 | return (
19 |
28 | {text ? (
29 |
30 |
31 | {text}
32 |
33 | ) : (
34 |
35 | )}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/components/common/propCard.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Toast, Typography } from "@douyinfe/semi-ui";
2 | import { IconCopy } from "@douyinfe/semi-icons";
3 | import React, { memo } from "react";
4 |
5 | export const PropCard = memo(({ propName, propValue }: { propName: string; propValue }) => {
6 | const { Text } = Typography;
7 | const content = Array.isArray(propValue) ? propValue.join(" ") : propValue;
8 | return (
9 | }
26 | style={{ marginRight: 10 }}
27 | size="small"
28 | onClick={() => {
29 | navigator.clipboard.writeText(content).then(
30 | () => {
31 | Toast.info("Copied to clipboard");
32 | },
33 | () => {
34 | Toast.error("Failed to copy");
35 | },
36 | );
37 | }}
38 | >
39 | }
40 | >
41 | {content}
42 |
43 | );
44 | });
45 |
--------------------------------------------------------------------------------
/frontend/src/components/common/sectionTitle.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from "@douyinfe/semi-ui";
2 | import { ReactNode } from "react";
3 |
4 | interface Props {
5 | title: ReactNode;
6 | }
7 |
8 | export function SectionTitle(props: Props) {
9 | return (
10 |
11 |
12 | {props.title}
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/components/layout/AppFooter.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Divider, Layout, Typography } from "@douyinfe/semi-ui";
3 | import Logo from "../logo";
4 |
5 | export default function AppFooter(): React.JSX.Element {
6 | return (
7 |
17 |
18 |
19 |
20 | © 2023 - {new Date().getFullYear()} Neet Design. All rights reserved.
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/components/layout/AppHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Typography, Space, Button, Layout } from "@douyinfe/semi-ui";
2 | import { Link } from "react-router-dom";
3 | import SwitchColorMode from "../themeSwitcher";
4 | import { useTitle } from "../appTitle";
5 |
6 | export default function AppHeader() {
7 | const { title, extra } = useTitle();
8 | return (
9 |
18 |
19 |

20 |
NEETBOX
21 |
22 | {title}
23 | {extra}
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/components/layout/ConsoleLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "@douyinfe/semi-ui";
2 | import { Outlet } from "react-router-dom";
3 | import ConsoleNavBar from "../../pages/console/sidebar";
4 | import { ErrorBoundary } from "../common/errorBoundary";
5 | import AppFooter from "./AppFooter";
6 |
7 | export default function ConsoleLayout() {
8 | const { Sider, Content } = Layout;
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/components/logo.module.css:
--------------------------------------------------------------------------------
1 | body {
2 | --logo-glow-color: rgb(255, 255, 255);
3 | }
4 |
5 | .neet-logo-glow {
6 | transition:
7 | filter 0.3s ease-in-out,
8 | opacity 0.3s ease-in-out,
9 | transform 0.3s ease-in-out;
10 | opacity: 0.7;
11 | }
12 |
13 | .neet-logo-glow:hover {
14 | filter: drop-shadow(0 0 0.55rem var(--logo-glow-color));
15 | opacity: 1;
16 | transform: rotate(360deg);
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/components/logo.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from "react";
2 | import styles from "./logo.module.css";
3 |
4 | type LogoProps = {
5 | styles?: CSSProperties;
6 | className?: string;
7 | withGlow?: boolean;
8 | withLink?: boolean;
9 | withTitle?: boolean;
10 | };
11 |
12 | export default function Logo(props: LogoProps) {
13 | const { withLink = false, withGlow = false } = props;
14 | const url = withLink ? "https://neetbox.550w.host" : undefined;
15 | const glowStyleClassName = withGlow ? styles["neet-logo-glow"] : undefined;
16 | const combinedStyles: CSSProperties = {
17 | ...{}, // add default style here
18 | ...props.styles,
19 | };
20 | const imageComponent = (
21 |
26 | );
27 | return (
28 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/components/overview/serverProps.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Descriptions, Card, Toast, Typography } from "@douyinfe/semi-ui";
3 | import { useAPI } from "../../services/api";
4 |
5 | export function ServerPropsCard(): React.JSX.Element {
6 | const { Text } = Typography;
7 |
8 | const { data: serverIPs } = useAPI("/server/listips", { refreshInterval: 5000 });
9 | const hostname = serverIPs?.hostname;
10 | const ips = serverIPs?.ips || [];
11 |
12 | const { data: serverVersion } = useAPI("/server/version", { refreshInterval: 5000 });
13 | const version = serverVersion?.version;
14 |
15 | const { data: configs } = useAPI("/server/configs", { refreshInterval: 5000 });
16 |
17 | const copyToClipboard = (value: string) => {
18 | navigator.clipboard
19 | .writeText(value)
20 | .then(() => {
21 | Toast.info("Copied to clipboard");
22 | })
23 | .catch(() => {
24 | Toast.error("Failed to copy");
25 | });
26 | };
27 |
28 | const mapValueToStyle = (value: any) => {
29 | if (typeof value === "boolean") {
30 | return {value ? "True" : "False"};
31 | }
32 | return value;
33 | };
34 |
35 | const data = [
36 | { key: "Hostname", value: hostname },
37 | ...ips.map((ip) => ({
38 | key: "IP",
39 | value: (
40 | copyToClipboard(ip)}
42 | style={{ cursor: "pointer", display: "flex", alignItems: "center" }}
43 | >
44 | {ip}
45 |
46 | ),
47 | })),
48 | { key: "Server Version", value: version },
49 | // configs
50 | ...Object.entries(configs || {}).map(([key, value]) => ({
51 | key: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, " $1"),
52 | value: (
53 | copyToClipboard(String(value))}
55 | style={{ cursor: "pointer", display: "flex", alignItems: "center" }}
56 | >
57 | {mapValueToStyle(value)}
58 |
59 | ),
60 | })),
61 | ];
62 |
63 | return (
64 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hardware/cpugraph.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { ECharts, getSemiColorDataHexColors } from "../../common/echarts";
3 | import { CpuInfo } from "../../../services/types";
4 | import { TimeDataMapper } from "../../../utils/timeDataMapper";
5 | import { getTimeAxisOptions } from "./utils";
6 | import { GraphWrapper } from "./graphWrapper";
7 |
8 | export const CPUGraph = ({ data }: { data: TimeDataMapper }) => {
9 | const cpus = data.getValue(0);
10 | const initialOption = () => {
11 | return {
12 | color: getSemiColorDataHexColors(false),
13 | backgroundColor: "transparent",
14 | animation: false,
15 | tooltip: {
16 | trigger: "axis",
17 | },
18 | grid: {
19 | top: 30,
20 | bottom: 30,
21 | },
22 | title: {
23 | left: 20,
24 | text: `CPU (${cpus.length} threads)`,
25 | textStyle: {
26 | fontSize: 12,
27 | },
28 | },
29 | // legend: {
30 | // data: cpus.map((cpu) => `CPU${cpu.id}`),
31 | // },
32 | xAxis: {
33 | type: "time",
34 | },
35 | yAxis: {
36 | type: "value",
37 | max: cpus.length * 100,
38 | axisLabel: {
39 | formatter: (x) => x + " %",
40 | },
41 | },
42 | series: [],
43 | } as echarts.EChartsOption;
44 | };
45 |
46 | const updatingOption = useCallback(() => {
47 | const newOption = {
48 | series: cpus.map((cpu) => ({
49 | name: `CPU${cpu.id}`,
50 | type: "line",
51 | stack: "cpu",
52 | areaStyle: {},
53 | symbol: null,
54 | data: data.map((timestamp, cpus) => [new Date(timestamp), cpus[cpu.id].percentage]),
55 | })),
56 | xAxis: getTimeAxisOptions(data),
57 | } as echarts.EChartsOption;
58 | return newOption;
59 | }, [cpus, data]);
60 |
61 | return (
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hardware/gpugraph.css:
--------------------------------------------------------------------------------
1 | .gpu-temperature {
2 | --temperature-bg-brightness: 80%;
3 | position: absolute;
4 | right: 10%;
5 | border-radius: 5px;
6 | padding: 1px 5px;
7 | }
8 | [theme-mode="dark"] .gpu-temperature {
9 | --temperature-bg-brightness: 30%;
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hardware/graphWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, memo } from "react";
2 | import { JsonPopover } from "../jsonView";
3 |
4 | export const GraphWrapper = memo(
5 | ({ title, lastValue, children }: PropsWithChildren<{ title: string; lastValue: any }>) => {
6 | return (
7 |
8 | {children}
9 |
15 |
16 | );
17 | },
18 | );
19 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hardware/index.tsx:
--------------------------------------------------------------------------------
1 | import { Typography } from "@douyinfe/semi-ui";
2 | import { HardwareInfo } from "../../../services/types";
3 | import { useCurrentProject, useProjectData } from "../../../hooks/useProject";
4 | import Loading from "../../common/loading";
5 | import { TimeDataMapper } from "../../../utils/timeDataMapper";
6 | import { CPUGraph } from "./cpugraph";
7 | import { GPUGraph } from "./gpugraph";
8 | import { RAMGraph } from "./ramgraph";
9 | import { fetchDataCount } from "./utils";
10 |
11 | export function Hardware() {
12 | const { projectId, runId } = useCurrentProject();
13 | const data = useProjectData({
14 | projectId,
15 | runId,
16 | type: "hardware",
17 | transformHTTP: (x) => ({ timestamp: x.timestamp, ...x.metadata }),
18 | transformWS: (x) => ({ timestamp: x.timestamp, ...x.payload }),
19 | limit: fetchDataCount,
20 | });
21 | return data?.length ? (
22 |
23 | {data.every((x) => x.gpus?.length) ? (
24 | data[0].gpus.map((_, i) => x.gpus[i])} />)
25 | ) : (
26 |
27 | )}
28 | {data.every((x) => x.cpus.length) ? (
29 | x.cpus)} />
30 | ) : (
31 |
32 | )}
33 | {data.every((x) => x.ram) ? (
34 | x.ram)} />
35 | ) : (
36 |
37 | )}
38 |
39 | ) : (
40 |
41 | );
42 | }
43 |
44 | function NoInfoLabel({ text }: { text: string }) {
45 | return (
46 |
47 | {text}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hardware/ramgraph.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { ECharts, getSemiColorDataHexColors } from "../../common/echarts";
3 | import { RamInfo } from "../../../services/types";
4 | import { TimeDataMapper } from "../../../utils/timeDataMapper";
5 | import { getTimeAxisOptions } from "./utils";
6 | import { GraphWrapper } from "./graphWrapper";
7 |
8 | export const RAMGraph = ({ data }: { data: TimeDataMapper }) => {
9 | const initialOption = () => {
10 | return {
11 | color: getSemiColorDataHexColors(false),
12 | backgroundColor: "transparent",
13 | animation: false,
14 | tooltip: {
15 | trigger: "axis",
16 | },
17 | title: {
18 | left: 20,
19 | text: `RAM`,
20 | textStyle: {
21 | fontSize: 12,
22 | },
23 | },
24 | grid: {
25 | top: 30,
26 | bottom: 30,
27 | },
28 | legend: {
29 | data: [`RAM Used`],
30 | },
31 | xAxis: {
32 | type: "time",
33 | },
34 | yAxis: [
35 | {
36 | type: "value",
37 | position: "right",
38 | axisLabel: {
39 | formatter: (x) => (x / 1e3).toFixed(1) + " GB",
40 | },
41 | max: data.getValue(0).total,
42 | },
43 | ],
44 | series: [],
45 | } as echarts.EChartsOption;
46 | };
47 |
48 | const updatingOption = useCallback(() => {
49 | const newOption = {
50 | series: [
51 | {
52 | name: `RAM Used(GB)`,
53 | type: "line",
54 | areaStyle: {},
55 | symbol: null,
56 | data: data.map((timestamp, ram) => [new Date(timestamp), ram.used]),
57 | },
58 | ],
59 | xAxis: getTimeAxisOptions(data),
60 | } as echarts.EChartsOption;
61 | return newOption;
62 | }, [data]);
63 |
64 | return (
65 |
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hardware/utils.ts:
--------------------------------------------------------------------------------
1 | import { TimeDataMapper } from "../../../utils/timeDataMapper";
2 |
3 | const viewRangeSeconds = 300;
4 | const dataInterval = 2;
5 | export const fetchDataCount = Math.ceil((viewRangeSeconds / dataInterval) * 1.1);
6 |
7 | export function getTimeAxisOptions(mapper: TimeDataMapper) {
8 | const latestTime = new Date(mapper.data[mapper.data.length - 1].timestamp).getTime();
9 | return {
10 | min: latestTime - viewRangeSeconds * 1000,
11 | max: latestTime,
12 | };
13 | }
14 |
15 | export function percent2hue(value) {
16 | return ((100 - value) * 1.2).toString(10);
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/components/project/hyperParams.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import { Popover, Space, Typography } from "@douyinfe/semi-ui";
3 | import { IconInfoCircle } from "@douyinfe/semi-icons";
4 | import { useProjectRunStatus } from "../../hooks/useProject";
5 | import Loading from "../common/loading";
6 | import { JsonViewThemed } from "./jsonView";
7 |
8 | export const HyperParams = memo(
9 | ({ projectId, runId, trigger = "click", position, children = }: any) => {
10 | return (
11 | }
16 | >
17 | {children}
18 |
19 | );
20 | },
21 | );
22 |
23 | const HyperParamsContent = memo(({ projectId, runId }: any) => {
24 | const [runStatus] = useProjectRunStatus(projectId, runId);
25 | const value = runStatus?.hyperparameters;
26 | return (
27 |
28 | Hyperparameter
29 | {!runStatus ? (
30 |
31 | ) : value == null ? (
32 | N/A
33 | ) : (
34 |
35 | )}
36 |
37 | );
38 | });
39 |
--------------------------------------------------------------------------------
/frontend/src/components/project/imagesAndScatters.tsx:
--------------------------------------------------------------------------------
1 | import { Space } from "@douyinfe/semi-ui";
2 | import { memo } from "react";
3 | import { AllImageViewers } from "./images";
4 | import { AllScatterViewers } from "./charts/scatters";
5 |
6 | export const ImagesAndScatters = memo(() => {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | });
14 |
--------------------------------------------------------------------------------
/frontend/src/components/project/jsonView.css:
--------------------------------------------------------------------------------
1 | .w-json-view-container {
2 | padding-left: 1em;
3 | }
4 |
5 | span[style="display: inline-flex; align-items: center;"] {
6 | /* position: absolute; */
7 | transform: translate(-1em, 0);
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/components/project/jsonView.tsx:
--------------------------------------------------------------------------------
1 | import JsonView from "@uiw/react-json-view";
2 | // eslint-disable-next-line import/no-unresolved
3 | import { githubDarkTheme } from "@uiw/react-json-view/githubDark";
4 | import { CSSProperties, memo } from "react";
5 | import { Popover, Space, Typography } from "@douyinfe/semi-ui";
6 | import { IconCodeStroked } from "@douyinfe/semi-icons";
7 | import { Position } from "@douyinfe/semi-ui/lib/es/tooltip";
8 | import { useTheme } from "../../hooks/useTheme";
9 | import "./jsonView.css";
10 |
11 | export const JsonViewThemed = memo((props: any) => {
12 | const { darkMode } = useTheme();
13 | return (
14 |
27 | );
28 | });
29 |
30 | export const JsonPopover = memo(
31 | ({
32 | value,
33 | title,
34 | position,
35 | style,
36 | }: {
37 | value: any;
38 | title?: string;
39 | position?: Position;
40 | style?: CSSProperties;
41 | }) => {
42 | return (
43 |
48 | {title && {title}}
49 |
50 |
51 | }
52 | >
53 |
54 |
55 | );
56 | },
57 | );
58 |
--------------------------------------------------------------------------------
/frontend/src/components/project/logs/logs.css:
--------------------------------------------------------------------------------
1 | @import "../../../../public/fonts/maple/maple.css";
2 |
3 | .log-item {
4 | margin-bottom: 5px;
5 | font-family: "MapleMono", monospace;
6 | font-family: "MapleMono", monospace;
7 | white-space: pre-wrap;
8 | font-size: 13px;
9 |
10 | .log-tag {
11 | display: inline-block;
12 | --log-tag-bg-hs: 0, 0%;
13 | --log-tag-bg-l: 80%;
14 | background-color: hsl(var(--log-tag-bg-hs), var(--log-tag-bg-l));
15 | padding: 0 3px;
16 | border-radius: 5px;
17 | }
18 |
19 | .log-whom {
20 | --log-tag-bg-l: 30%;
21 | background-color: rgba(var(--semi-grey-1));
22 | }
23 |
24 | .log-prefix-info {
25 | background-color: var(--semi-color-info);
26 | color: var(--semi-color-default);
27 | }
28 |
29 | .log-prefix-mention {
30 | background-color: var(--semi-color-secondary);
31 | color: var(--semi-color-default);
32 | }
33 |
34 | .log-prefix-ok {
35 | background-color: var(--semi-color-success);
36 | color: var(--semi-color-default);
37 | }
38 |
39 | .log-prefix-warning {
40 | background-color: var(--semi-color-warning);
41 | color: var(--semi-color-default);
42 | }
43 |
44 | .log-prefix-debug {
45 | background-color: var(--semi-color-link);
46 | color: var(--semi-color-default);
47 | }
48 |
49 | .log-prefix-error {
50 | background-color: var(--semi-color-danger);
51 | color: var(--semi-color-default);
52 | }
53 | }
54 |
55 | [theme-mode="dark"] .log-item .log-tag {
56 | --log-tag-bg-l: 30%;
57 | }
58 |
59 | [theme-mode="dark"] .log-item .log-whom {
60 | --log-tag-bg-l: 80%;
61 | }
62 |
--------------------------------------------------------------------------------
/frontend/src/components/themeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useLayoutEffect, useState } from "react";
2 | import { flushSync } from "react-dom";
3 | import { Switch } from "@douyinfe/semi-ui";
4 | import { ThemeContext, useTheme } from "../hooks/useTheme";
5 |
6 | export default function SwitchColorMode(): React.JSX.Element {
7 | const { darkMode, setDarkMode } = useTheme();
8 | const switchMode = (checked, e) => {
9 | setDarkMode(!checked, e.nativeEvent);
10 | };
11 | return (
12 |
20 | );
21 | }
22 |
23 | export function ThemeContextProvider(props: React.PropsWithChildren) {
24 | const [darkMode, setDarkModeState] = useState(() => localStorage.getItem("neetbox-theme") != "light");
25 |
26 | const setDarkMode = useCallback((val, mouseEvent) => {
27 | const setTheme = () => {
28 | setDarkModeState(val);
29 | localStorage.setItem("neetbox-theme", val ? "" : "light");
30 | };
31 |
32 | if (document.startViewTransition) {
33 | document.startViewTransition(() => {
34 | flushSync(() => {
35 | setTheme();
36 | document.documentElement.style.setProperty(
37 | "--page-theme-changing-origin",
38 | typeof mouseEvent?.x === "number" ? `${mouseEvent.x}px ${mouseEvent.y}px` : "",
39 | );
40 | });
41 | });
42 | } else {
43 | setTheme();
44 | }
45 | }, []);
46 |
47 | useLayoutEffect(() => {
48 | const body = document.body;
49 | if (darkMode) {
50 | body.setAttribute("theme-mode", "dark");
51 | } else {
52 | body.removeAttribute("theme-mode");
53 | }
54 | }, [darkMode]);
55 |
56 | return {props.children};
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useMemoJSON.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 |
3 | /** Return the same ref if the data is not changed */
4 | export function useMemoJSON(data: T): T {
5 | // eslint-disable-next-line react-hooks/exhaustive-deps
6 | return useMemo(() => data, [JSON.stringify(data)]);
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useProject.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect } from "react";
2 | import { useAtom } from "jotai";
3 | import { KeyedMutator } from "swr";
4 | import { getProject } from "../services/projects";
5 | import { WsMsg } from "../services/projectWebsocket";
6 | import { useAPI } from "../services/api";
7 | import { RunStatus } from "../services/types";
8 | import { useProjectData } from "./useProjectData";
9 |
10 | export { useProjectData };
11 |
12 | export const ProjectContext = createContext<{
13 | projectId: string;
14 | projectName?: string;
15 | runId?: string;
16 | isOnlineRun: boolean;
17 | } | null>(null);
18 |
19 | export function useCurrentProject() {
20 | return useContext(ProjectContext)!;
21 | }
22 |
23 | export function useProjectStatus(id: string) {
24 | return useAPI(`/project/${id}`, { refreshInterval: 5000 });
25 | }
26 |
27 | export function useProjectRunIds(id: string) {
28 | const { data, mutate } = useAPI(`/project/${id}`, { refreshInterval: 5000 });
29 | return { data: data?.runids, mutate };
30 | }
31 |
32 | export function useProjectRunStatus(
33 | id: string,
34 | runId?: string,
35 | ): [data: RunStatus | undefined, mutate: KeyedMutator] {
36 | const { data, mutate } = useAPI(`/project/${id}/run/${runId}`, { refreshInterval: 5000 });
37 | return [!runId ? undefined : data, mutate];
38 | }
39 |
40 | export function useProjectWebSocketReady(id: string) {
41 | const project = getProject(id);
42 | return useAtom(project.wsClient.isReady.atom)[0];
43 | }
44 |
45 | export function useProjectWebSocket(
46 | id: string,
47 | type: T | null,
48 | onMessage: (msg: Extract) => void,
49 | ) {
50 | const project = getProject(id);
51 | useEffect(() => {
52 | const handle: typeof onMessage = (msg) => {
53 | if (!type || msg.eventType == type) {
54 | onMessage(msg);
55 | }
56 | };
57 | project.wsClient.wsListeners.add(handle as any);
58 | return () => void project.wsClient.wsListeners.delete(handle as any);
59 | }, [project, type, onMessage]);
60 | }
61 |
62 | export function useProjectSeries(projectId: string, runId: string, type: string) {
63 | return useProjectData({
64 | type: `${type}`,
65 | url: `/project/${projectId}/series/${type}?${new URLSearchParams({ runId: runId })}`,
66 | projectId,
67 | runId,
68 | transformWS: (msg) => msg.series,
69 | reducer: (data, queue) => [...new Set([...data, ...queue])],
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useReportError.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { addNotice } from "../utils/notification";
3 |
4 | export function useReportGlobalError() {
5 | useEffect(() => {
6 | const handleError = (e: WindowEventMap["error"]) => {
7 | showError(e.message);
8 | };
9 | const handleRejection = (e: WindowEventMap["unhandledrejection"]) => {
10 | showError(String(e.reason));
11 | };
12 | window.addEventListener("error", handleError);
13 | window.addEventListener("unhandledrejection", handleRejection);
14 | return () => {
15 | window.removeEventListener("error", handleError);
16 | window.removeEventListener("unhandledrejection", handleRejection);
17 | };
18 | }, []);
19 | }
20 |
21 | let errorCount = 0;
22 |
23 | function showError(errorText: string) {
24 | errorCount++;
25 | addNotice({
26 | id: "app-error",
27 | type: "error",
28 | title: `Frontend App Error (${errorCount})`,
29 | content: (
30 |
39 | {errorText}
40 |
41 | ),
42 | duration: 10,
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useTheme.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | export const ThemeContext = createContext<{
4 | darkMode: boolean;
5 | setDarkMode: (val: boolean, mouseEvent?: Event) => void;
6 | }>(null!);
7 |
8 | export function useTheme() {
9 | return useContext(ThemeContext);
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | @media (prefers-color-scheme: light) {
6 | :root {
7 | color: #213547;
8 | background-color: #ffffff;
9 | }
10 | a:hover {
11 | color: #747bff;
12 | }
13 | button {
14 | background-color: #f9f9f9;
15 | }
16 | }
17 |
18 | :root {
19 | --page-theme-changing-origin: 50% 50%;
20 | }
21 | ::view-transition-old(root) {
22 | animation: none;
23 | mix-blend-mode: normal;
24 | }
25 | ::view-transition-new(root) {
26 | animation: page-theme-changing 0.7s ease-in;
27 | mix-blend-mode: normal;
28 | }
29 | @keyframes page-theme-changing {
30 | 0% {
31 | clip-path: circle(0% at var(--page-theme-changing-origin));
32 | }
33 | 100% {
34 | clip-path: circle(141% at var(--page-theme-changing-origin));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { RouterProvider, createBrowserRouter } from "react-router-dom";
4 | import { LocaleProvider } from "@douyinfe/semi-ui";
5 | import en_US from "@douyinfe/semi-ui/lib/es/locale/source/en_US";
6 | import LoginPage from "./pages/login";
7 | import "./index.css";
8 | import { consoleRoutes } from "./pages/console";
9 | import { ThemeContextProvider } from "./components/themeSwitcher";
10 | import { ServiceProvider } from "./services/serviceProvider";
11 | import App from "./App";
12 | import { RouteError } from "./components/common/errorBoundary";
13 |
14 | const router = createBrowserRouter(
15 | [
16 | {
17 | path: "/",
18 | element: ,
19 | errorElement: ,
20 | children: [
21 | // {
22 | // path: "",
23 | // element: ,
24 | // },
25 | consoleRoutes(),
26 | {
27 | path: "/login",
28 | element: ,
29 | },
30 | ],
31 | },
32 | ],
33 | { basename: "/web/" },
34 | );
35 |
36 | if (process.env.NODE_ENV === "development") {
37 | if (window.location.pathname == "/") {
38 | // For dev only. This is bad because it restarts the whole page/script loading process.
39 | // In production it's done by server redirect.
40 | window.location.replace("/web/");
41 | }
42 | }
43 |
44 | ReactDOM.createRoot(document.getElementById("root")!).render(
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ,
54 | );
55 |
--------------------------------------------------------------------------------
/frontend/src/pages/console/index.tsx:
--------------------------------------------------------------------------------
1 | import { RouteObject } from "react-router-dom";
2 | import ConsoleLayout from "../../components/layout/ConsoleLayout";
3 | import { RouteError } from "../../components/common/errorBoundary";
4 | import Dashboard from "./projectDashboard";
5 | import Overview from "./overview";
6 |
7 | export function consoleRoutes(): RouteObject {
8 | return {
9 | path: "",
10 | element: ,
11 | children: [
12 | { path: "", element: },
13 | {
14 | path: "project/:projectId",
15 | element: ,
16 | errorElement: ,
17 | },
18 | ],
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/pages/console/overview.tsx:
--------------------------------------------------------------------------------
1 | import { CardGroup, Divider } from "@douyinfe/semi-ui";
2 | import { SectionTitle } from "../../components/common/sectionTitle";
3 | import { ServerPropsCard } from "../../components/overview/serverProps";
4 | import { DiskUsageCard } from "../../components/overview/diskUsage";
5 | import ProjectList from "../../components/overview/projectList";
6 |
7 | export default function Overview() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/pages/console/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Nav, Space, Tag, Typography } from "@douyinfe/semi-ui";
2 | import { IconHome, IconListView, IconGlobeStroke } from "@douyinfe/semi-icons";
3 | import { useLocation, useNavigate } from "react-router-dom";
4 | import { useAPI } from "../../services/api";
5 | import Loading from "../../components/common/loading";
6 | import "./sidebarStyleFix.css";
7 |
8 | export default function ConsoleNavBar() {
9 | const location = useLocation();
10 | const navigate = useNavigate();
11 | const { data } = useAPI("/project/list", { refreshInterval: 5000 });
12 | return (
13 |