From 59ab1419369c636dedb38412a082507525eab527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E7=AB=A5=E5=B4=87?= <792998983@qq.com> Date: Thu, 1 Feb 2024 23:34:03 +0800 Subject: [PATCH] feat(ai): scow ai (#1017) next+trpc+orm+openApi --------- Co-authored-by: ZihanChen821 <827625357@qq.com> Co-authored-by: Chen Junda Co-authored-by: OYX-1 <13121812323@163.com> Co-authored-by: picca Sun Co-authored-by: Miracle575 Co-authored-by: Yixin Sun <43978285+piccaSun@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: ZihanChen821 <130351655+ZihanChen821@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .eslintignore | 2 + .vscode/settings.json | 14 +- apps/ai/.eslintignore | 15 + apps/ai/.eslintrc.json | 3 + apps/ai/.gitignore | 43 + apps/ai/README.md | 6 + apps/ai/assets/app/server_entry.sh | 11 + apps/ai/assets/app/vnc_entry.sh | 1 + apps/ai/assets/icons/192.png | Bin 0 -> 1366 bytes apps/ai/assets/icons/512.png | Bin 0 -> 3877 bytes apps/ai/assets/icons/favicon.ico | Bin 0 -> 4286 bytes apps/ai/assets/logo/banner.dark.svg | 1 + apps/ai/assets/logo/banner.svg | 1 + apps/ai/assets/logo/logo.dark.svg | 1 + apps/ai/assets/logo/logo.svg | 1 + apps/ai/config/ai/apps/jupter.yaml | 43 + apps/ai/config/ai/config.yaml | 12 + apps/ai/config/apps/emacs.yaml | 40 + apps/ai/config/apps/vscode.yaml | 67 + apps/ai/config/audit.yaml | 8 + apps/ai/config/clusters/hpc01.yaml | 34 + apps/ai/config/clusters/hpc02.yaml | 30 + apps/ai/config/common.yaml | 28 + apps/ai/config/portal.yaml | 85 + apps/ai/config/ui.yaml | 2 + apps/ai/env/.env.dev | 3 + apps/ai/launch.sh | 24 + apps/ai/next.config.mjs | 87 + apps/ai/package.json | 124 + .../app/(auth)/algorithm/AlgorithmTable.tsx | 250 ++ .../(auth)/algorithm/AlgorithmVersionList.tsx | 213 ++ .../algorithm/CopyPublicAlgorithmModal.tsx | 154 ++ .../algorithm/CreateAndEditAlgorithmModal.tsx | 177 ++ .../algorithm/CreateAndEditVersionModal.tsx | 196 ++ apps/ai/src/app/(auth)/algorithm/page.tsx | 17 + .../src/app/(auth)/algorithm/private/page.tsx | 28 + .../src/app/(auth)/algorithm/public/page.tsx | 28 + apps/ai/src/app/(auth)/context.ts | 34 + apps/ai/src/app/(auth)/dashboard/page.tsx | 50 + .../(auth)/dataset/CopyPublicDatasetModal.tsx | 155 ++ .../dataset/CreateEditDSVersionModal.tsx | 187 ++ .../(auth)/dataset/CreateEditDatasetModal.tsx | 202 ++ .../app/(auth)/dataset/DatasetListTable.tsx | 252 ++ .../app/(auth)/dataset/DatasetVersionList.tsx | 213 ++ apps/ai/src/app/(auth)/dataset/page.tsx | 17 + .../src/app/(auth)/dataset/private/page.tsx | 30 + .../ai/src/app/(auth)/dataset/public/page.tsx | 30 + .../src/app/(auth)/defaultClusterContext.ts | 35 + .../src/app/(auth)/files/CreateFileModal.tsx | 81 + apps/ai/src/app/(auth)/files/FileManager.tsx | 485 ++++ apps/ai/src/app/(auth)/files/FileTable.tsx | 138 ++ apps/ai/src/app/(auth)/files/PathBar.tsx | 108 + apps/ai/src/app/(auth)/files/RenameModal.tsx | 78 + .../files/[cluster]/[[...path]]/page.tsx | 75 + .../app/(auth)/files/[cluster]/context.tsx | 35 + .../src/app/(auth)/files/[cluster]/layout.tsx | 29 + apps/ai/src/app/(auth)/files/api.ts | 39 + apps/ai/src/app/(auth)/files/upload/route.ts | 91 + .../src/app/(auth)/image/CopyImageModal.tsx | 105 + .../app/(auth)/image/CreateEditImageModal.tsx | 265 ++ .../src/app/(auth)/image/ImageListTable.tsx | 278 +++ apps/ai/src/app/(auth)/image/page.tsx | 17 + apps/ai/src/app/(auth)/image/private/page.tsx | 30 + apps/ai/src/app/(auth)/image/public/page.tsx | 30 + .../jobs/[clusterId]/AppSessionsTable.tsx | 299 +++ .../jobs/[clusterId]/ConnectToAppLink.tsx | 112 + .../(auth)/jobs/[clusterId]/LaunchAppForm.tsx | 715 ++++++ .../jobs/[clusterId]/SaveImageModal.tsx | 112 + .../jobs/[clusterId]/SelectAppTable.tsx | 121 + .../[clusterId]/createApps/[appId]/page.tsx | 51 + .../jobs/[clusterId]/createApps/page.tsx | 59 + .../jobs/[clusterId]/historyJobs/page.tsx | 40 + .../src/app/(auth)/jobs/[clusterId]/hooks.ts | 73 + .../jobs/[clusterId]/runningJobs/page.tsx | 41 + .../jobs/[clusterId]/trainJobs/page.tsx | 52 + apps/ai/src/app/(auth)/layout.tsx | 122 + .../app/(auth)/model/CopyPublicModelModal.tsx | 158 ++ .../(auth)/model/CreateAndEditModelModal.tsx | 195 ++ .../model/CreateAndEditVersionModal.tsx | 204 ++ apps/ai/src/app/(auth)/model/ModelTable.tsx | 225 ++ .../src/app/(auth)/model/ModelVersionList.tsx | 213 ++ apps/ai/src/app/(auth)/model/page.tsx | 17 + apps/ai/src/app/(auth)/model/private/page.tsx | 28 + apps/ai/src/app/(auth)/model/public/page.tsx | 27 + .../(auth)/profile/ChangePasswordModal.tsx | 108 + apps/ai/src/app/(auth)/profile/page.tsx | 105 + apps/ai/src/app/(auth)/routes.tsx | 200 ++ apps/ai/src/app/(auth)/swagger/page.tsx | 33 + apps/ai/src/app/auth.ts | 39 + apps/ai/src/app/clientLayout.tsx | 81 + apps/ai/src/app/layout.tsx | 46 + apps/ai/src/app/page.tsx | 17 + apps/ai/src/app/trpcClient.server.tsx | 41 + apps/ai/src/app/trpcClient.tsx | 103 + apps/ai/src/app/uiContext.ts | 25 + apps/ai/src/components/AccountSelector.tsx | 55 + apps/ai/src/components/ClusterSelector.tsx | 98 + apps/ai/src/components/DecompressionModal.tsx | 75 + apps/ai/src/components/DisabledA.tsx | 42 + apps/ai/src/components/ErrorBoundary.tsx | 68 + apps/ai/src/components/FileSelectModal.tsx | 366 +++ apps/ai/src/components/FileTable.tsx | 140 ++ .../ai/src/components/FilterFormContainer.tsx | 15 + apps/ai/src/components/LanguageSwitcher.tsx | 81 + apps/ai/src/components/Loading.tsx | 31 + apps/ai/src/components/MkdirModal.tsx | 78 + apps/ai/src/components/ModalLink.tsx | 71 + apps/ai/src/components/PageTitle.tsx | 52 + apps/ai/src/components/PathBar.tsx | 105 + apps/ai/src/components/Redirect.tsx | 31 + apps/ai/src/components/Section.tsx | 52 + apps/ai/src/components/TableTitle.tsx | 19 + apps/ai/src/components/TopProgressBar.tsx | 52 + apps/ai/src/components/UploadModal.tsx | 144 ++ apps/ai/src/components/icons/README.md | 13 + apps/ai/src/components/icons/moon.svg | 9 + apps/ai/src/components/icons/sun-moon.svg | 12 + apps/ai/src/components/icons/sun.svg | 25 + apps/ai/src/i18n/en.ts | 535 ++++ apps/ai/src/i18n/index.ts | 56 + apps/ai/src/i18n/zh_cn.ts | 533 ++++ apps/ai/src/layouts/AntdConfigProvider.tsx | 57 + apps/ai/src/layouts/AppFloatButtons.tsx | 26 + apps/ai/src/layouts/base/BaseLayout.tsx | 118 + apps/ai/src/layouts/base/FormLayout.tsx | 45 + apps/ai/src/layouts/base/NavItemProps.d.ts | 25 + apps/ai/src/layouts/base/SideNav/BodyMask.tsx | 48 + apps/ai/src/layouts/base/SideNav/index.tsx | 139 ++ apps/ai/src/layouts/base/common.tsx | 86 + apps/ai/src/layouts/base/constants.ts | 35 + .../src/layouts/base/header/BigScreenMenu.tsx | 68 + .../ai/src/layouts/base/header/Components.tsx | 63 + apps/ai/src/layouts/base/header/Logo.tsx | 48 + .../src/layouts/base/header/UserIndicator.tsx | 109 + apps/ai/src/layouts/base/header/index.tsx | 118 + apps/ai/src/layouts/base/matchers.ts | 31 + apps/ai/src/layouts/constants.ts | 14 + apps/ai/src/layouts/darkMode.tsx | 105 + apps/ai/src/layouts/error/ForbiddenPage.tsx | 38 + .../src/layouts/error/NotAuthorizedPage.tsx | 37 + apps/ai/src/layouts/error/NotFoundPage.tsx | 29 + .../ai/src/layouts/error/RootErrorContent.tsx | 24 + apps/ai/src/layouts/error/ServerErrorPage.tsx | 30 + .../layouts/styleRegistry/AntdRegistry.tsx | 39 + .../StyledComponentsRegistry.tsx | 41 + apps/ai/src/models/Algorithm.ts | 33 + apps/ai/src/models/Cluster.ts | 28 + apps/ai/src/models/Dateset.ts | 43 + apps/ai/src/models/File.ts | 21 + apps/ai/src/models/Image.ts | 32 + apps/ai/src/models/Job.ts | 17 + apps/ai/src/models/Model.ts | 18 + apps/ai/src/models/User.ts | 16 + apps/ai/src/models/common.ts | 18 + apps/ai/src/models/operationLog.ts | 81 + apps/ai/src/pages/api/[...trpc].ts | 30 + apps/ai/src/pages/api/openapi.json.ts | 21 + .../[type]/[host]/[port]/[[...path]].ts | 57 + apps/ai/src/pages/api/setup.ts | 28 + apps/ai/src/pages/api/trpc/[trpc].ts | 32 + apps/ai/src/server/auth/cookie.ts | 45 + apps/ai/src/server/auth/server.ts | 50 + apps/ai/src/server/auth/token.ts | 42 + apps/ai/src/server/config/ai.ts | 15 + apps/ai/src/server/config/apps.ts | 15 + apps/ai/src/server/config/clusters.ts | 17 + apps/ai/src/server/config/common.ts | 15 + apps/ai/src/server/config/db.ts | 44 + apps/ai/src/server/config/env.ts | 64 + apps/ai/src/server/config/portal.ts | 18 + apps/ai/src/server/config/ui.ts | 15 + apps/ai/src/server/entities/Algorithm.ts | 93 + .../src/server/entities/AlgorithmVersion.ts | 83 + apps/ai/src/server/entities/Dataset.ts | 78 + apps/ai/src/server/entities/DatasetVersion.ts | 77 + apps/ai/src/server/entities/Image.ts | 104 + apps/ai/src/server/entities/Model.ts | 87 + apps/ai/src/server/entities/ModelVersion.ts | 82 + apps/ai/src/server/entities/index.ts | 30 + .../server/migrations/.snapshot-scow_ai.json | 906 +++++++ .../migrations/Migration20240102091246.ts | 52 + .../migrations/Migration20240103020827.ts | 15 + .../migrations/Migration20240103024536.ts | 13 + .../migrations/Migration20240103072610.ts | 15 + .../migrations/Migration20240112074625.ts | 43 + .../migrations/Migration20240126070152.ts | 17 + .../migrations/Migration20240129034339.ts | 33 + .../migrations/Migration20240131083455.ts | 33 + apps/ai/src/server/migrations/index.ts | 33 + apps/ai/src/server/mikro-orm.config.ts | 17 + apps/ai/src/server/setup/proxy.ts | 125 + apps/ai/src/server/trpc/context.ts | 39 + apps/ai/src/server/trpc/def.ts | 26 + .../server/trpc/middleware/withAuthContext.ts | 49 + .../server/trpc/middleware/withOrmContext.ts | 26 + apps/ai/src/server/trpc/openapi.ts | 25 + apps/ai/src/server/trpc/procedure/base.ts | 20 + apps/ai/src/server/trpc/route/account.ts | 55 + .../server/trpc/route/algorithm/algorithm.ts | 273 +++ .../trpc/route/algorithm/algorithmVersion.ts | 539 ++++ .../src/server/trpc/route/algorithm/index.ts | 32 + apps/ai/src/server/trpc/route/auth.ts | 175 ++ apps/ai/src/server/trpc/route/config.ts | 239 ++ .../src/server/trpc/route/dataset/dataset.ts | 296 +++ .../trpc/route/dataset/datasetVersion.ts | 569 +++++ .../ai/src/server/trpc/route/dataset/index.ts | 33 + apps/ai/src/server/trpc/route/file.ts | 447 ++++ apps/ai/src/server/trpc/route/image/image.ts | 612 +++++ apps/ai/src/server/trpc/route/image/index.ts | 25 + apps/ai/src/server/trpc/route/jobs/apps.ts | 819 +++++++ apps/ai/src/server/trpc/route/jobs/index.ts | 36 + apps/ai/src/server/trpc/route/jobs/jobs.ts | 198 ++ apps/ai/src/server/trpc/route/logo.ts | 56 + apps/ai/src/server/trpc/route/model/index.ts | 31 + apps/ai/src/server/trpc/route/model/model.ts | 277 +++ .../server/trpc/route/model/modelVersion.ts | 558 +++++ apps/ai/src/server/trpc/route/utils.ts | 34 + apps/ai/src/server/trpc/router.ts | 41 + apps/ai/src/server/utils/app.ts | 132 + .../src/server/utils/checkPathPermission.ts | 130 + apps/ai/src/server/utils/chmod.ts | 36 + apps/ai/src/server/utils/clusters.ts | 24 + apps/ai/src/server/utils/copyFile.ts | 37 + apps/ai/src/server/utils/error.ts | 21 + apps/ai/src/server/utils/errorCode.ts | 21 + apps/ai/src/server/utils/errors.ts | 21 + apps/ai/src/server/utils/getOrm.ts | 41 + apps/ai/src/server/utils/image.ts | 126 + apps/ai/src/server/utils/logger.ts | 24 + apps/ai/src/server/utils/orm.ts | 46 + apps/ai/src/server/utils/pagination.ts | 48 + apps/ai/src/server/utils/share.ts | 207 ++ apps/ai/src/server/utils/ssh.ts | 89 + apps/ai/src/styles/constants.ts | 34 + apps/ai/src/styles/globals.css | 15 + apps/ai/src/types/styled-components.d.ts | 23 + apps/ai/src/utils/array.ts | 48 + apps/ai/src/utils/common.ts | 30 + apps/ai/src/utils/datetime.ts | 83 + apps/ai/src/utils/file.ts | 45 + apps/ai/src/utils/form.ts | 53 + apps/ai/src/utils/format.ts | 44 + apps/ai/src/utils/head.tsx | 26 + apps/ai/src/utils/hooks.ts | 26 + apps/ai/src/utils/isPortReachable.ts | 46 + apps/ai/src/utils/math.ts | 24 + apps/ai/src/utils/pagination.ts | 37 + apps/ai/src/utils/parse.ts | 70 + apps/ai/src/utils/processEnv.ts | 17 + apps/ai/src/utils/trpc.ts | 160 ++ apps/ai/src/utils/url.ts | 47 + apps/ai/src/utils/vnc.ts | 62 + apps/ai/start.mjs | 46 + apps/ai/tsconfig.build.json | 7 + apps/ai/tsconfig.json | 53 + apps/ai/tsconfig.server.json | 15 + apps/cli/src/cmd/checkConfig.ts | 8 + apps/cli/src/cmd/enterAiDb.ts | 29 + apps/cli/src/cmd/migrate.ts | 10 + apps/cli/src/compose/index.ts | 51 + apps/cli/src/config/install.ts | 12 +- apps/cli/src/index.ts | 5 + apps/cli/tests/compose.test.ts | 14 + apps/gateway/assets/nginx.conf | 8 + apps/gateway/src/env.ts | 3 + apps/mis-web/config.js | 5 + apps/mis-web/env/.env.dev | 2 + apps/mis-web/src/i18n/en.ts | 3 +- apps/mis-web/src/i18n/zh_cn.ts | 3 +- apps/mis-web/src/layouts/BaseLayout.tsx | 10 +- apps/mis-web/src/utils/config.ts | 2 + apps/portal-server/src/clusterops/app.ts | 36 +- apps/portal-server/src/services/config.ts | 3 +- apps/portal-server/src/services/job.ts | 3 +- apps/portal-server/src/utils/clusters.ts | 61 - apps/portal-web/config.js | 5 + apps/portal-web/env/.env.dev | 2 + apps/portal-web/src/i18n/en.ts | 3 +- apps/portal-web/src/i18n/zh_cn.ts | 3 +- apps/portal-web/src/layouts/BaseLayout.tsx | 10 +- .../src/pages/apps/[clusterId]/createApps.tsx | 5 +- apps/portal-web/src/utils/config.ts | 2 + dev/test-adapter/src/services/app.ts | 7 + dev/test-adapter/src/services/job.ts | 1 - dev/vagrant/config/ai/apps/jupyter.yaml | 43 + dev/vagrant/config/ai/apps/vscode.yaml | 38 + dev/vagrant/config/ai/config.yaml | 56 + dev/vagrant/pm2.config.js | 24 + docker/Dockerfile.scow | 2 +- docs/docs/deploy/config/ai/_category_.json | 8 + .../deploy/config/ai/apps/_category_.json | 8 + .../config/ai/apps/apps/_category_.json | 8 + .../config/ai/apps/apps/jupyterlab/index.md | 75 + .../config/ai/apps/apps/vscode/index.md | 66 + .../config/ai/apps/configure-web-app.md | 116 + docs/docs/deploy/config/ai/apps/intro.md | 13 + docs/docs/deploy/config/ai/intro.md | 110 + docs/docs/deploy/config/audit/_category_.json | 2 +- docs/docs/deploy/config/cli/_category_.json | 2 +- .../config/customization/_category_.json | 2 +- .../deploy/config/gateway/_category_.json | 2 +- docs/docs/deploy/config/mis/_category_.json | 2 +- .../config/multi-cluster/_category_.json | 8 + .../docs/deploy/config/multi-cluster/index.md | 2 +- .../docs/deploy/config/portal/_category_.json | 2 +- docs/docs/info/ai/_category_.json | 4 + docs/docs/info/ai/index.md | 28 + libs/config/src/ai.ts | 66 + libs/config/src/appForAi.ts | 100 + libs/config/src/mis.ts | 2 + libs/config/src/portal.ts | 3 + libs/protos/scheduler-adapter/package.json | 2 +- libs/server/package.json | 13 +- libs/server/src/app.ts | 52 + libs/server/src/index.ts | 2 + libs/server/src/scheduleAdapter.ts | 75 + .../web/src/layouts/base/SideNav/BodyMask.tsx | 1 + libs/web/src/layouts/globalStyle.tsx | 1 + libs/web/src/layouts/icon.tsx | 1 + pnpm-lock.yaml | 2175 ++++++++++++++++- scripts/copyDist.mjs | 2 +- scripts/version.mjs | 2 + 322 files changed, 26135 insertions(+), 215 deletions(-) create mode 100644 apps/ai/.eslintignore create mode 100644 apps/ai/.eslintrc.json create mode 100644 apps/ai/.gitignore create mode 100644 apps/ai/README.md create mode 100644 apps/ai/assets/app/server_entry.sh create mode 100644 apps/ai/assets/app/vnc_entry.sh create mode 100644 apps/ai/assets/icons/192.png create mode 100644 apps/ai/assets/icons/512.png create mode 100644 apps/ai/assets/icons/favicon.ico create mode 100644 apps/ai/assets/logo/banner.dark.svg create mode 100644 apps/ai/assets/logo/banner.svg create mode 100644 apps/ai/assets/logo/logo.dark.svg create mode 100644 apps/ai/assets/logo/logo.svg create mode 100644 apps/ai/config/ai/apps/jupter.yaml create mode 100644 apps/ai/config/ai/config.yaml create mode 100644 apps/ai/config/apps/emacs.yaml create mode 100644 apps/ai/config/apps/vscode.yaml create mode 100644 apps/ai/config/audit.yaml create mode 100644 apps/ai/config/clusters/hpc01.yaml create mode 100644 apps/ai/config/clusters/hpc02.yaml create mode 100644 apps/ai/config/common.yaml create mode 100644 apps/ai/config/portal.yaml create mode 100644 apps/ai/config/ui.yaml create mode 100644 apps/ai/env/.env.dev create mode 100755 apps/ai/launch.sh create mode 100644 apps/ai/next.config.mjs create mode 100644 apps/ai/package.json create mode 100644 apps/ai/src/app/(auth)/algorithm/AlgorithmTable.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/AlgorithmVersionList.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/CopyPublicAlgorithmModal.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/CreateAndEditAlgorithmModal.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/CreateAndEditVersionModal.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/page.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/private/page.tsx create mode 100644 apps/ai/src/app/(auth)/algorithm/public/page.tsx create mode 100644 apps/ai/src/app/(auth)/context.ts create mode 100644 apps/ai/src/app/(auth)/dashboard/page.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/CopyPublicDatasetModal.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/CreateEditDSVersionModal.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/CreateEditDatasetModal.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/DatasetListTable.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/DatasetVersionList.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/page.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/private/page.tsx create mode 100644 apps/ai/src/app/(auth)/dataset/public/page.tsx create mode 100644 apps/ai/src/app/(auth)/defaultClusterContext.ts create mode 100644 apps/ai/src/app/(auth)/files/CreateFileModal.tsx create mode 100644 apps/ai/src/app/(auth)/files/FileManager.tsx create mode 100644 apps/ai/src/app/(auth)/files/FileTable.tsx create mode 100644 apps/ai/src/app/(auth)/files/PathBar.tsx create mode 100644 apps/ai/src/app/(auth)/files/RenameModal.tsx create mode 100644 apps/ai/src/app/(auth)/files/[cluster]/[[...path]]/page.tsx create mode 100644 apps/ai/src/app/(auth)/files/[cluster]/context.tsx create mode 100644 apps/ai/src/app/(auth)/files/[cluster]/layout.tsx create mode 100644 apps/ai/src/app/(auth)/files/api.ts create mode 100644 apps/ai/src/app/(auth)/files/upload/route.ts create mode 100644 apps/ai/src/app/(auth)/image/CopyImageModal.tsx create mode 100644 apps/ai/src/app/(auth)/image/CreateEditImageModal.tsx create mode 100644 apps/ai/src/app/(auth)/image/ImageListTable.tsx create mode 100644 apps/ai/src/app/(auth)/image/page.tsx create mode 100644 apps/ai/src/app/(auth)/image/private/page.tsx create mode 100644 apps/ai/src/app/(auth)/image/public/page.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/AppSessionsTable.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/ConnectToAppLink.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/LaunchAppForm.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/SaveImageModal.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/SelectAppTable.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/[appId]/page.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/page.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/historyJobs/page.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/hooks.ts create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/runningJobs/page.tsx create mode 100644 apps/ai/src/app/(auth)/jobs/[clusterId]/trainJobs/page.tsx create mode 100644 apps/ai/src/app/(auth)/layout.tsx create mode 100644 apps/ai/src/app/(auth)/model/CopyPublicModelModal.tsx create mode 100644 apps/ai/src/app/(auth)/model/CreateAndEditModelModal.tsx create mode 100644 apps/ai/src/app/(auth)/model/CreateAndEditVersionModal.tsx create mode 100644 apps/ai/src/app/(auth)/model/ModelTable.tsx create mode 100644 apps/ai/src/app/(auth)/model/ModelVersionList.tsx create mode 100644 apps/ai/src/app/(auth)/model/page.tsx create mode 100644 apps/ai/src/app/(auth)/model/private/page.tsx create mode 100644 apps/ai/src/app/(auth)/model/public/page.tsx create mode 100644 apps/ai/src/app/(auth)/profile/ChangePasswordModal.tsx create mode 100644 apps/ai/src/app/(auth)/profile/page.tsx create mode 100644 apps/ai/src/app/(auth)/routes.tsx create mode 100644 apps/ai/src/app/(auth)/swagger/page.tsx create mode 100644 apps/ai/src/app/auth.ts create mode 100644 apps/ai/src/app/clientLayout.tsx create mode 100644 apps/ai/src/app/layout.tsx create mode 100644 apps/ai/src/app/page.tsx create mode 100644 apps/ai/src/app/trpcClient.server.tsx create mode 100644 apps/ai/src/app/trpcClient.tsx create mode 100644 apps/ai/src/app/uiContext.ts create mode 100644 apps/ai/src/components/AccountSelector.tsx create mode 100644 apps/ai/src/components/ClusterSelector.tsx create mode 100644 apps/ai/src/components/DecompressionModal.tsx create mode 100644 apps/ai/src/components/DisabledA.tsx create mode 100644 apps/ai/src/components/ErrorBoundary.tsx create mode 100644 apps/ai/src/components/FileSelectModal.tsx create mode 100644 apps/ai/src/components/FileTable.tsx create mode 100644 apps/ai/src/components/FilterFormContainer.tsx create mode 100644 apps/ai/src/components/LanguageSwitcher.tsx create mode 100644 apps/ai/src/components/Loading.tsx create mode 100644 apps/ai/src/components/MkdirModal.tsx create mode 100644 apps/ai/src/components/ModalLink.tsx create mode 100644 apps/ai/src/components/PageTitle.tsx create mode 100644 apps/ai/src/components/PathBar.tsx create mode 100644 apps/ai/src/components/Redirect.tsx create mode 100644 apps/ai/src/components/Section.tsx create mode 100644 apps/ai/src/components/TableTitle.tsx create mode 100644 apps/ai/src/components/TopProgressBar.tsx create mode 100644 apps/ai/src/components/UploadModal.tsx create mode 100644 apps/ai/src/components/icons/README.md create mode 100644 apps/ai/src/components/icons/moon.svg create mode 100644 apps/ai/src/components/icons/sun-moon.svg create mode 100644 apps/ai/src/components/icons/sun.svg create mode 100644 apps/ai/src/i18n/en.ts create mode 100644 apps/ai/src/i18n/index.ts create mode 100644 apps/ai/src/i18n/zh_cn.ts create mode 100644 apps/ai/src/layouts/AntdConfigProvider.tsx create mode 100644 apps/ai/src/layouts/AppFloatButtons.tsx create mode 100644 apps/ai/src/layouts/base/BaseLayout.tsx create mode 100644 apps/ai/src/layouts/base/FormLayout.tsx create mode 100644 apps/ai/src/layouts/base/NavItemProps.d.ts create mode 100644 apps/ai/src/layouts/base/SideNav/BodyMask.tsx create mode 100644 apps/ai/src/layouts/base/SideNav/index.tsx create mode 100644 apps/ai/src/layouts/base/common.tsx create mode 100644 apps/ai/src/layouts/base/constants.ts create mode 100644 apps/ai/src/layouts/base/header/BigScreenMenu.tsx create mode 100644 apps/ai/src/layouts/base/header/Components.tsx create mode 100644 apps/ai/src/layouts/base/header/Logo.tsx create mode 100644 apps/ai/src/layouts/base/header/UserIndicator.tsx create mode 100644 apps/ai/src/layouts/base/header/index.tsx create mode 100644 apps/ai/src/layouts/base/matchers.ts create mode 100644 apps/ai/src/layouts/constants.ts create mode 100644 apps/ai/src/layouts/darkMode.tsx create mode 100644 apps/ai/src/layouts/error/ForbiddenPage.tsx create mode 100644 apps/ai/src/layouts/error/NotAuthorizedPage.tsx create mode 100644 apps/ai/src/layouts/error/NotFoundPage.tsx create mode 100644 apps/ai/src/layouts/error/RootErrorContent.tsx create mode 100644 apps/ai/src/layouts/error/ServerErrorPage.tsx create mode 100644 apps/ai/src/layouts/styleRegistry/AntdRegistry.tsx create mode 100644 apps/ai/src/layouts/styleRegistry/StyledComponentsRegistry.tsx create mode 100644 apps/ai/src/models/Algorithm.ts create mode 100644 apps/ai/src/models/Cluster.ts create mode 100644 apps/ai/src/models/Dateset.ts create mode 100644 apps/ai/src/models/File.ts create mode 100644 apps/ai/src/models/Image.ts create mode 100644 apps/ai/src/models/Job.ts create mode 100644 apps/ai/src/models/Model.ts create mode 100644 apps/ai/src/models/User.ts create mode 100644 apps/ai/src/models/common.ts create mode 100644 apps/ai/src/models/operationLog.ts create mode 100644 apps/ai/src/pages/api/[...trpc].ts create mode 100644 apps/ai/src/pages/api/openapi.json.ts create mode 100644 apps/ai/src/pages/api/proxy/[clusterId]/[type]/[host]/[port]/[[...path]].ts create mode 100644 apps/ai/src/pages/api/setup.ts create mode 100644 apps/ai/src/pages/api/trpc/[trpc].ts create mode 100644 apps/ai/src/server/auth/cookie.ts create mode 100644 apps/ai/src/server/auth/server.ts create mode 100644 apps/ai/src/server/auth/token.ts create mode 100644 apps/ai/src/server/config/ai.ts create mode 100644 apps/ai/src/server/config/apps.ts create mode 100644 apps/ai/src/server/config/clusters.ts create mode 100644 apps/ai/src/server/config/common.ts create mode 100644 apps/ai/src/server/config/db.ts create mode 100644 apps/ai/src/server/config/env.ts create mode 100644 apps/ai/src/server/config/portal.ts create mode 100644 apps/ai/src/server/config/ui.ts create mode 100644 apps/ai/src/server/entities/Algorithm.ts create mode 100644 apps/ai/src/server/entities/AlgorithmVersion.ts create mode 100644 apps/ai/src/server/entities/Dataset.ts create mode 100644 apps/ai/src/server/entities/DatasetVersion.ts create mode 100644 apps/ai/src/server/entities/Image.ts create mode 100644 apps/ai/src/server/entities/Model.ts create mode 100644 apps/ai/src/server/entities/ModelVersion.ts create mode 100644 apps/ai/src/server/entities/index.ts create mode 100644 apps/ai/src/server/migrations/.snapshot-scow_ai.json create mode 100644 apps/ai/src/server/migrations/Migration20240102091246.ts create mode 100644 apps/ai/src/server/migrations/Migration20240103020827.ts create mode 100644 apps/ai/src/server/migrations/Migration20240103024536.ts create mode 100644 apps/ai/src/server/migrations/Migration20240103072610.ts create mode 100644 apps/ai/src/server/migrations/Migration20240112074625.ts create mode 100644 apps/ai/src/server/migrations/Migration20240126070152.ts create mode 100644 apps/ai/src/server/migrations/Migration20240129034339.ts create mode 100644 apps/ai/src/server/migrations/Migration20240131083455.ts create mode 100644 apps/ai/src/server/migrations/index.ts create mode 100644 apps/ai/src/server/mikro-orm.config.ts create mode 100644 apps/ai/src/server/setup/proxy.ts create mode 100644 apps/ai/src/server/trpc/context.ts create mode 100644 apps/ai/src/server/trpc/def.ts create mode 100644 apps/ai/src/server/trpc/middleware/withAuthContext.ts create mode 100644 apps/ai/src/server/trpc/middleware/withOrmContext.ts create mode 100644 apps/ai/src/server/trpc/openapi.ts create mode 100644 apps/ai/src/server/trpc/procedure/base.ts create mode 100644 apps/ai/src/server/trpc/route/account.ts create mode 100644 apps/ai/src/server/trpc/route/algorithm/algorithm.ts create mode 100644 apps/ai/src/server/trpc/route/algorithm/algorithmVersion.ts create mode 100644 apps/ai/src/server/trpc/route/algorithm/index.ts create mode 100644 apps/ai/src/server/trpc/route/auth.ts create mode 100644 apps/ai/src/server/trpc/route/config.ts create mode 100644 apps/ai/src/server/trpc/route/dataset/dataset.ts create mode 100644 apps/ai/src/server/trpc/route/dataset/datasetVersion.ts create mode 100644 apps/ai/src/server/trpc/route/dataset/index.ts create mode 100644 apps/ai/src/server/trpc/route/file.ts create mode 100644 apps/ai/src/server/trpc/route/image/image.ts create mode 100644 apps/ai/src/server/trpc/route/image/index.ts create mode 100644 apps/ai/src/server/trpc/route/jobs/apps.ts create mode 100644 apps/ai/src/server/trpc/route/jobs/index.ts create mode 100644 apps/ai/src/server/trpc/route/jobs/jobs.ts create mode 100644 apps/ai/src/server/trpc/route/logo.ts create mode 100644 apps/ai/src/server/trpc/route/model/index.ts create mode 100644 apps/ai/src/server/trpc/route/model/model.ts create mode 100644 apps/ai/src/server/trpc/route/model/modelVersion.ts create mode 100644 apps/ai/src/server/trpc/route/utils.ts create mode 100644 apps/ai/src/server/trpc/router.ts create mode 100644 apps/ai/src/server/utils/app.ts create mode 100644 apps/ai/src/server/utils/checkPathPermission.ts create mode 100644 apps/ai/src/server/utils/chmod.ts create mode 100644 apps/ai/src/server/utils/clusters.ts create mode 100644 apps/ai/src/server/utils/copyFile.ts create mode 100644 apps/ai/src/server/utils/error.ts create mode 100644 apps/ai/src/server/utils/errorCode.ts create mode 100644 apps/ai/src/server/utils/errors.ts create mode 100644 apps/ai/src/server/utils/getOrm.ts create mode 100644 apps/ai/src/server/utils/image.ts create mode 100644 apps/ai/src/server/utils/logger.ts create mode 100644 apps/ai/src/server/utils/orm.ts create mode 100644 apps/ai/src/server/utils/pagination.ts create mode 100644 apps/ai/src/server/utils/share.ts create mode 100644 apps/ai/src/server/utils/ssh.ts create mode 100644 apps/ai/src/styles/constants.ts create mode 100644 apps/ai/src/styles/globals.css create mode 100644 apps/ai/src/types/styled-components.d.ts create mode 100644 apps/ai/src/utils/array.ts create mode 100644 apps/ai/src/utils/common.ts create mode 100644 apps/ai/src/utils/datetime.ts create mode 100644 apps/ai/src/utils/file.ts create mode 100644 apps/ai/src/utils/form.ts create mode 100644 apps/ai/src/utils/format.ts create mode 100644 apps/ai/src/utils/head.tsx create mode 100644 apps/ai/src/utils/hooks.ts create mode 100644 apps/ai/src/utils/isPortReachable.ts create mode 100644 apps/ai/src/utils/math.ts create mode 100644 apps/ai/src/utils/pagination.ts create mode 100644 apps/ai/src/utils/parse.ts create mode 100644 apps/ai/src/utils/processEnv.ts create mode 100644 apps/ai/src/utils/trpc.ts create mode 100644 apps/ai/src/utils/url.ts create mode 100644 apps/ai/src/utils/vnc.ts create mode 100644 apps/ai/start.mjs create mode 100644 apps/ai/tsconfig.build.json create mode 100644 apps/ai/tsconfig.json create mode 100644 apps/ai/tsconfig.server.json create mode 100644 apps/cli/src/cmd/enterAiDb.ts create mode 100644 dev/vagrant/config/ai/apps/jupyter.yaml create mode 100644 dev/vagrant/config/ai/apps/vscode.yaml create mode 100644 dev/vagrant/config/ai/config.yaml create mode 100644 docs/docs/deploy/config/ai/_category_.json create mode 100644 docs/docs/deploy/config/ai/apps/_category_.json create mode 100644 docs/docs/deploy/config/ai/apps/apps/_category_.json create mode 100644 docs/docs/deploy/config/ai/apps/apps/jupyterlab/index.md create mode 100644 docs/docs/deploy/config/ai/apps/apps/vscode/index.md create mode 100644 docs/docs/deploy/config/ai/apps/configure-web-app.md create mode 100644 docs/docs/deploy/config/ai/apps/intro.md create mode 100644 docs/docs/deploy/config/ai/intro.md create mode 100644 docs/docs/deploy/config/multi-cluster/_category_.json create mode 100644 docs/docs/info/ai/_category_.json create mode 100644 docs/docs/info/ai/index.md create mode 100644 libs/config/src/ai.ts create mode 100644 libs/config/src/appForAi.ts create mode 100644 libs/server/src/app.ts create mode 100644 libs/server/src/scheduleAdapter.ts diff --git a/.eslintignore b/.eslintignore index 35eb79f0af..934b282dbb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,3 +11,5 @@ **/next-env.d.ts **/build + +**/generated diff --git a/.vscode/settings.json b/.vscode/settings.json index 91f01cfdf8..2a4856d976 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,17 @@ "--proto_path=./protos" ], }, - "buf.binaryPath": "node_modules/@bufbuild/buf/bin/buf" + "buf.binaryPath": "node_modules/@bufbuild/buf/bin/buf", + "cSpell.words": [ + "trpc" + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], } diff --git a/apps/ai/.eslintignore b/apps/ai/.eslintignore new file mode 100644 index 0000000000..e62fc5850a --- /dev/null +++ b/apps/ai/.eslintignore @@ -0,0 +1,15 @@ +# don't ever lint node_modules + +**/node_modules +# don't lint build output (make sure it's set to your correct build folder name) +**/out + +**/build/ + +**/migrations + +**/server/migrations + +**/next-env.d.ts + +**/build diff --git a/apps/ai/.eslintrc.json b/apps/ai/.eslintrc.json new file mode 100644 index 0000000000..232a18ebdb --- /dev/null +++ b/apps/ai/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@ddadaal/eslint-config/react" +} diff --git a/apps/ai/.gitignore b/apps/ai/.gitignore new file mode 100644 index 0000000000..738c003917 --- /dev/null +++ b/apps/ai/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +/.turbo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.vscode + +public/novnc + +.next.backup diff --git a/apps/ai/README.md b/apps/ai/README.md new file mode 100644 index 0000000000..07e2c483cc --- /dev/null +++ b/apps/ai/README.md @@ -0,0 +1,6 @@ +ai服务 + +主要实现: +next + trpc + mikro-orm + openApi + +支持trpc和http 两种调用 \ No newline at end of file diff --git a/apps/ai/assets/app/server_entry.sh b/apps/ai/assets/app/server_entry.sh new file mode 100644 index 0000000000..6873f48017 --- /dev/null +++ b/apps/ai/assets/app/server_entry.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# $1: the length of password +function get_password { + local password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c$1) + echo $password +} + +export PORT=$1 +export HOST=$2 +export SVCPORT=$3 diff --git a/apps/ai/assets/app/vnc_entry.sh b/apps/ai/assets/app/vnc_entry.sh new file mode 100644 index 0000000000..8ad1dfb535 --- /dev/null +++ b/apps/ai/assets/app/vnc_entry.sh @@ -0,0 +1 @@ +//TODO diff --git a/apps/ai/assets/icons/192.png b/apps/ai/assets/icons/192.png new file mode 100644 index 0000000000000000000000000000000000000000..a25d21254b3da992984b1bd4ab21774796c63cfc GIT binary patch literal 1366 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zcaloCO|{#S9F5Kqj{dBd==Vdmu}) z#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDFz0X$DS^ZAr*7p-r3t7lPb~n z@%5WE7g-mxEK*w28Z8mP?HD7N^C!!tiAB_Ve^{=mXJX>U$sZKXCaf)BaF(!Xn8A z>`1roq1RUdt?y5zV%EwnI;8ZW>Z`!2s|UWfusy(|3zumFBCZ?8U%(Llzj6Aic8NuT ze@st)o|f>{s%O`{bo=fPDa`ie8^es-wsp6rEo=>bf4Ki@@rr8|&yW6363_nnN@zu6 zukVh-Gk*0QsC;n4q`n5|oxGUFi|=;-zrw4^(C}GqqD0&44`v^J z=zNn3V)%Zy!T754ivqnvLDq+7&vE8rcyLBy+UoWTPx{`?`TXk7iNkSiVh4miWPTNx zbw*7l-OF9>o6!~qhI@CLj<0IJaPaNC_q)oERr8(8VAKPKQuEC=(Fe_u@2!t8WITML zTCuK*@9w_Y8ypYro#V*HP{3_wqjZt~{z0t|QAh7J^D;#24f*1J_23p`i%%P-E3!Lq zOUq51ksns=-c;7I;O=R_P%QY+WO@iPyYJsdiU~kxl`q%UtY~`mbptW z&7}YN8+)v_9<|d5`*cCBoTjP4xHR;$_A?uct*OwmNY5nfJ@2;BQFaKd>Uy+9| zUSd~&NUtfry(i&L_GmCK``okrtLw572D!4k@>$Jb*3_5`pP26JoZaTr=y79GYtfeTQ$sBCUu2T_NjQ>&z6U(Cz}RKQoS0A@0S)r(LL| z&hIO#V%h=(mLccu(TO_jg_h+I2|6E_%7!8}mxf6`{BmhJ(^&Q^tVU^)OH`6iiZgPL zrVtv$$;-eDQ8R@3K^9l*JJ2NV+0(zGnSb3)bq^%-#AlErF+`!3btMdRcDW}+Ou0Zb ztp%yuQZ4DS7p=N4V}Bb_t7U=LFY^f&H)xwcz1dEb`%YJgNfx}G58LdC)P>`5z9gU6 zBJ5@nIt`6|hY~XnA|q5~Qop4te`q#=D1$BP+pl?#jBd0vlEB z7f2%`cT7*I{}{ZU{Lw6xQ6LGY&jO65ZV0XcC(=stXVhps2M2vyy%|dx##dU_0myg+ zb$-+S&2o{2fDsT#7j6=$*V~C4j%2TK(dk;=m`6t@ATv6t`$b*;c^l^vfSS8#X z1q^TJm6%602Q)mmHD4X9hL+n%-A^v^Lh7~J zf1f&ng37s13Hyb;74~QfGS{Q0aKeBg*Mwp*90`bHI!i~3ux2ewi~zmihiKw+c>JCx zS!gfPnq&}H)4+CR>IP$v-Vsdb`mv{La&sWc0D^+=VSMZfc8-?rrX2msPL#;>rjJ3P zUB<6rhBd}_dWwgw=vXXKIDSE0?_`*qG-@zW%(q!{poueoBD48MUl%NARi^ zla5JQP&WUQ@#ve_xu7_c^O)HaahJ_J04w?eH>uCloZ?U756%6oMQt85C?b&cG}m7(bTDM)@2(&W0?i4-^C$YoLPz zuYzZTXN1r7l!*|K@ooG{w0{A8(U0v%Y@n?(SyX;s$jp5X58rWSUGCaKrm!&#{IZWJ z&JD8e>`csLpvr1~gA+;Bu@TABFmZ~~f_G>o>cou}4>w0!g2v+;OmCV=zQcvsY^u3h zm;iVGYbC1YOGluouE~oTqc#n7ygm2JNtvy+??53YrnUPC)Z2-dnr%_&WC{{Yg?8bx z$QCb_F)xZN>LtE&dZ=P-mk-LZ0vLM(NmYNCcCRv`%@@|^|8jf-N*1!&a}DUA1O9?n zv}Rt~-o71rbpmdWJz*b8sHBW#Lir$qor9)wpbM$R+Gb9=_5nZoQg!>`HU>$xLmrn? zY2BbmLE=X-{G2VgtsY{%=Xf^#yG4pWVU|0Df{ehHmRUU2qCswtPI7~`2-LO9KtiKr zO7XQ-!;K9bVQVAhvipkOQzh&I4ernsu3@?t;ob_b#K(%X*i6$ncBh&TXeOr6nHrPm zM^Y6Fhzo7;tK%%vxl>$XSj+M)2D)&$qW>a~*OVf4iY101RswXNdm^&`gwKB=c4mgZHwbz^?pNDmocg7%&q67cZbqZXKo`P`-TBMwp7`_CG&@&y0DzqLV zuP)lXX!IZl?>kNw?W#b!Ch4$tEcDGvb8|3LxO4|-S{z@5jYCt12WTEL2=A0yC$RBJ z=^$*pY=Z;uJ%cGMf3);+%{2tCnqgTrbSDxQ7pBEcHi->~YDd`e!9anfPt4ft$Q>#y zOD{{%G5a{0xDQ<#1Eh1@*(&oLOR^}LRj^UPH`wwL&t8=!y!~N%pB=c8R>IbY`ZVon zjA<*uZdLO;F9P02(evp&Gkefjldx$KSyWbmwBNt#wpwY?=k@@tL-LOs!~syrLcW=8oP%^ue0SBY zVzz4j&Q7w(V-tNOYmJNlKgSSnMs6<*%LM-G>+T^Ycs=wyFO#-Q#KT(NK;+75&fd|n zB1{@fJko!X5n5@^B)Y0O36pi`k7oJYfS%I$@-{7nBl&X^{7wBSItmRgrjj`N6z;H} z;PJD{m>_?;Fj!zSnhVGAKVSs?F!+B^{kPtMdNWu~VB;UFV)Ee=7jVO(LvQco{r&%x C{jn$j literal 0 HcmV?d00001 diff --git a/apps/ai/assets/icons/favicon.ico b/apps/ai/assets/icons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..21f633dda0d4437393b70eae1ee6b6333ddcb421 GIT binary patch literal 4286 zcmeH~KQ9D96u{q|g2ewwNK~gH8Wk-OAHbhP(CK^&RMZzCx3h&xF7X9WX^~T^6rxjz zh-gTx-`rW*V$AHil~c@JemisX-n{qr?arGykpkaRN%U2!eIkP*G6oFE4CwLt2iaDe zNUhlT*|@4mBzJa14)79AVf*n9?7L$2{(b)a``5blx100&zo&+r!AS~EMS^vavVQ)f z=pTR0y`^>44U_zpc^l9TJ?RKKMD{4JA=OsT3%iJEh5i$meAveHJr6;$XlLvP=m)*n zsAly4Ozexz`Hn-?@dvZmddLvN5xSlE`B&}O;=!m7u;pCK-njQN?J}%pU=7^~-v0vg z2k0*)=4xB44fO&p|INtp;rCzi@6R9p{Po|@pSl+0iQccaMfTI4e-_YpF^^7Kw5@U0 z9`!z)r0>Xoyg!)Q3WF8yL$QA<{=KB8A6*FNXdQd5$!|QS_bfFGhj0y78M#6K9W1px z_GN18IUHgBrQL?jzDs8E8;^bazwrKDdHk(B&b6s+b^envu?(l9bJ>jU>Lf+heo=V& Gf8-Nd>QkNo literal 0 HcmV?d00001 diff --git a/apps/ai/assets/logo/banner.dark.svg b/apps/ai/assets/logo/banner.dark.svg new file mode 100644 index 0000000000..954c20a1be --- /dev/null +++ b/apps/ai/assets/logo/banner.dark.svg @@ -0,0 +1 @@ + diff --git a/apps/ai/assets/logo/banner.svg b/apps/ai/assets/logo/banner.svg new file mode 100644 index 0000000000..94cf18aba0 --- /dev/null +++ b/apps/ai/assets/logo/banner.svg @@ -0,0 +1 @@ + diff --git a/apps/ai/assets/logo/logo.dark.svg b/apps/ai/assets/logo/logo.dark.svg new file mode 100644 index 0000000000..f1e1bdeaf9 --- /dev/null +++ b/apps/ai/assets/logo/logo.dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/ai/assets/logo/logo.svg b/apps/ai/assets/logo/logo.svg new file mode 100644 index 0000000000..aab0dc549d --- /dev/null +++ b/apps/ai/assets/logo/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/ai/config/ai/apps/jupter.yaml b/apps/ai/config/ai/apps/jupter.yaml new file mode 100644 index 0000000000..e554a93007 --- /dev/null +++ b/apps/ai/config/ai/apps/jupter.yaml @@ -0,0 +1,43 @@ + # 这个应用的ID +id: jupyter + +# 这个应用的名字 +name: jupyter + +# logoPath: /apps/Jupyter.svg + +image: + # 镜像名称 + name: jupyter/minimal-notebook + # 镜像版本 + tag: latest + +# 指定应用类型为web +type: web +# Web应用的配置 +web: + # 准备脚本 + beforeScript: | + export PASSWORD=$(get_password 12) + export SALT=123 + export PASSWORD_SHA1="$(echo -n "${PASSWORD}${SALT}" | openssl dgst -sha1 | awk '{print $NF}')" + # 指明运行任务的脚本中的启动命令,用户在创建应用页面可以在脚本中替换该命令 + startCommand: + jupyter-lab + # 运行任务的脚本。可以使用准备脚本定义的 + script: | + jupyter-lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --ServerApp.port_retries=0 --PasswordIdentityProvider.hashed_password="sha1:${SALT}:${PASSWORD_SHA1}" --ServerApp.open_browser=False --ServerApp.base_url="${PROXY_BASE_PATH}/${HOST}/${SVCPORT}/" --ServerApp.allow_origin='*' --ServerApp.disable_check_xsrf=True --ServerApp.root_dir="${workingDir}" --allow-root + proxyType: absolute + # 如何连接应用 + connect: + method: POST + path: /login + formData: + password: "{{ PASSWORD }}" + +attributes: + - type: text + name: workingDir + label: 指定jupyter工作目录 + required: true + placeholder: "请填写绝对路径" diff --git a/apps/ai/config/ai/config.yaml b/apps/ai/config/ai/config.yaml new file mode 100644 index 0000000000..a985b90e26 --- /dev/null +++ b/apps/ai/config/ai/config.yaml @@ -0,0 +1,12 @@ +db: + host: localhost + port: 3306 + user: root + password: mysqlrootpassword + dbName: scow_ai + +harborConfig: + url: 10.0.0.xxx + project: projectName + user: user + password: password diff --git a/apps/ai/config/apps/emacs.yaml b/apps/ai/config/apps/emacs.yaml new file mode 100644 index 0000000000..e716fd0844 --- /dev/null +++ b/apps/ai/config/apps/emacs.yaml @@ -0,0 +1,40 @@ +# 这个应用的ID +id: emacs + +# 这个应用的名字 +name: emacs + +# 这个应用的图标文件在公共文件下的路径 +# logoPath: /test.svg + +# 指定应用类型,vnc或者web +type: vnc + +# 可以运行这个应用的节点地址 +# 如果不设置nodes,则所有节点都可以运行 +# nodes: +# - t001 +# - t002 + +# VNC应用的配置 +vnc: + # 此X Session的xstartup脚本 + xstartup: | + emacs -mm + + +# 交互式应用说明,选填。 +# 类型为string或国际化文本类型 +# appComment: "对上方交互式应用进行说明
# 利用代码块说明
" +# 国际化类型 +# appComment: +# i18n: +# default: | +#

对上方交互式应用进行说明

+#
# 利用代码块说明
+# en: | +#

Explanation of the app above

+#
# explanation with code
+# zh_cn: | +#

对上方交互式应用进行说明

+#
# 利用代码块说明
diff --git a/apps/ai/config/apps/vscode.yaml b/apps/ai/config/apps/vscode.yaml new file mode 100644 index 0000000000..6a5c845c5e --- /dev/null +++ b/apps/ai/config/apps/vscode.yaml @@ -0,0 +1,67 @@ +# 这个应用的ID +id: vscode + +# 这个应用的名字 +name: VSCode + +# 这个应用的图标文件在公共文件下的路径 +# logoPath: /test.svg + +# 指定应用类型为web +type: web + +web: + # 准备脚本 + beforeScript: | + export PORT=$(get_port) + export PASSWORD=$(get_password 12) + # 运行任务的脚本。可以使用准备脚本定义的 + script: | + PASSWORD=$PASSWORD /data/software/code-server/bin/code-server -vvv --bind-addr 0.0.0.0:$PORT --auth password + proxyType: relative + # 如何连接应用 + connect: + method: POST + path: /login + formData: + password: "{{ PASSWORD }}" + + +# 配置HTML表单,用户可以指定code-server版本 +attributes: + - type: select + name: selectVersion + label: 选择版本 + # label: + # i18n: + # default: 选择版本 + # en: Select Version + # zh_cn: 选择版本 + required: true # 用户必须选择一个版本 + placeholder: 选择code-server的版本 # 提示信息 + # placeholder: + # i18n: + # default: 选择code-server的版本 + # en: Choose the version of the code-server + # zh_cn: 选择code-server的版本 + select: + - value: code-server/4.8.0 + label: version 4.8.0 + - value: code-server/4.9.0 + label: version 4.9.0 + + - type: text + name: sbatchOptions + label: 其他sbatch参数 + # label: + # i18n: + # default: 其他sbatch参数 + # en: Other sbatch parameters + # zh_cn: 其他sbatch参数 + required: false + placeholder: "比如:--gpus gres:2 --time 10" + # placeholder: + # i18n: + # default: "比如:--gpus gres:2 --time 10" + # en: "For example: --gpus gres:2 --time 10" + # zh_cn: "比如:--gpus gres:2 --time 10" diff --git a/apps/ai/config/audit.yaml b/apps/ai/config/audit.yaml new file mode 100644 index 0000000000..d8fb0b3975 --- /dev/null +++ b/apps/ai/config/audit.yaml @@ -0,0 +1,8 @@ +db: + host: localhost + port: 3306 + user: root + password: mysqlrootpassword + dbName: scow_audit + + diff --git a/apps/ai/config/clusters/hpc01.yaml b/apps/ai/config/clusters/hpc01.yaml new file mode 100644 index 0000000000..0aba392bac --- /dev/null +++ b/apps/ai/config/clusters/hpc01.yaml @@ -0,0 +1,34 @@ +displayName: hpc01Name +adapterUrl: 0.0.0.0:6000 +loginNodes: + - name: loginNode01 + address: localhost:22222 +crossClusterFileTransfer: + enabled: true + transferNode: localhost:22222 +# slurm: +# loginNodes: +# - localhost:22222 +# partitions: +# - name: compute +# nodes: 3 +# mem: 262144 +# cores: 32 +# gpus: 0 +# qos: +# - low +# - normal +# - high +# comment: 说明 + +# - name: GPU +# nodes: 1 +# mem: 262144 +# cores: 48 +# gpus: 8 +# qos: +# - low +# - normal +# - high +# - highest +# comment: 说明 diff --git a/apps/ai/config/clusters/hpc02.yaml b/apps/ai/config/clusters/hpc02.yaml new file mode 100644 index 0000000000..209800c455 --- /dev/null +++ b/apps/ai/config/clusters/hpc02.yaml @@ -0,0 +1,30 @@ +displayName: hpc02Name +adapterUrl: 0.0.0.0:6000 +loginNodes: + - name: loginNode02 + address: localhost:22 +crossClusterFileTransfer: + enabled: true + transferNode: localhost:222 +# slurm: +# loginNodes: +# - localhost:22 +# partitions: +# - name: GPU +# nodes: 2 +# mem: 262144 +# cores: 29 +# gpus: 8 +# qos: +# - normal +# - high +# - highest +# comment: 说明 + +# - name: another +# nodes: 2 +# mem: 262144 +# cores: 29 +# gpus: 8 +# comment: 说明 + diff --git a/apps/ai/config/common.yaml b/apps/ai/config/common.yaml new file mode 100644 index 0000000000..6aa6b401ba --- /dev/null +++ b/apps/ai/config/common.yaml @@ -0,0 +1,28 @@ +# 创建用户、修改密码时,密码的规则。必须设置 +passwordPattern: + # 正则表达式。下面为默认值 + regex: ^(?=.*\d)(?=.*[a-zA-Z])(?=.*[`~!@#\$%^&*()_+\-[\];',./{}|:"<>?]).{8,}$ + + # 出错时的消息。下面为默认值 + errorMessage: 必须包含字母、数字和符号,长度大于等于8位 + + +# 设置系统语言 可选配置 类型为对象或字符串,默认值为对象类型 +# 1.systemLanguage对象类型 +# systemLanguage: +# # 可选,默认为true。 +# # 如果true,则SCOW在用户未手动选择语言时,自动优先根据cookies, 其次根据浏览器header判断语言,判断失败使用下方配置的default语言。 +# # 如果为false,则SCOW在首次进入系统用户未手动选择语言时使用下方配置的default语言, +# # 用户手动选择过语言之后优先从cookies中进行判断,cookies不存在合法语言信息则使用下方配置的默认语言。 +# autoDetectWhenUserNotSet: true +# # 默认语言,选填。 +# # 类型必须为当前系统合法语言["zh_cn","en"]的字符串枚举值 +# # 若没有配置,则默认为"zh_cn" +# default: "zh_cn" + +# 2.systemLanguage字符串类型 +# 若systemLanguage配置为字符串,类型必须指定为当前系统合法语言["zh_cn","en"]的字符串枚举值 +# SCOW直接使用此语言,不允许用户再进行语言切换 +# systemLanguage: "zh_cn" + + diff --git a/apps/ai/config/portal.yaml b/apps/ai/config/portal.yaml new file mode 100644 index 0000000000..905e2544fa --- /dev/null +++ b/apps/ai/config/portal.yaml @@ -0,0 +1,85 @@ +# 是否启用作业管理功能 +jobManagement: true + +# 登录节点桌面功能 +loginDesktop: + # 是否启用 + enabled: true + + wms: + - name: Xfce + wm: xfce + - name: MATE + wm: mate + - name: GNOME 3 + wm: "" + - name: GNOME 2 + wm: 2d + - name: KDE + wm: 1-kde-plasma-standard + - name: cinnamon + wm: cinnamon + + # 最多启动多少个桌面节点 + maxDesktops: 3 + +# 是否启用交互式任务功能 +apps: true + +# 主页标题 +homeTitle: + defaultText: "Super Computing on Web
Test" + hostnameMap: + a.com: "a.com's SCOW Deployment" + +# 主页文本 +homeText: + defaultText: "SCOW
Haha" + hostnameMap: + a.com: "a.com's SCOW" + +# 提交作业命令框中的提示语 +# submitJobPromptText: "#此处参数设置的优先级高于页面其它地方,两者冲突时以此处为准" + +# 如果部署了管理系统,请设置管理系统部署的路径 +# misPath: /mis + +# 是否启用终端功能 +shell: true +# 提交作业的默认工作目录。使用{name}代替作业名称。相对于用户的家目录 +# submitJobDefaultPwd: scow/jobs/{name} + +# 将保存的作业保存到什么位置。相对于用户家目录 +# savedJobsDir: scow/savedJobs + +# 将交互式任务的信息保存到什么位置。相对于用户的家目录 +# appJobsDir: scow/appData + +# TurboVNC的安装路径 +# turboVNCPath: /opt/TurboVNC + +# # 新增导航链接相关配置 +# navLinks: +# # 链接名 +# - text: "一级导航1" +# # 链接地址,一级导航链接地址为可选填,二级导航链接地址为必填 +# url: "" +# # 是否打开新的页面,可选填,默认值为false +# # openInNewPage: true +# # 自定义图标地址,可选填 +# # iconPath: "" +# # 二级导航,可选填 +# children: +# # 二级导航相关配置,与一级导航相同,但是url为必填配置,同时不允许再设置children +# - text: "二级导航1" +# url: "https://hahahaha1.1.com" +# # openInNewPage: true +# iconPath: "" +# - text: "二级导航2" +# url: "https://hahahaha1.2.com" +# # openInNewPage: true +# - text: "一级导航2" +# url: "https://hahahaha2.com" +# # openInNewPage: true +# children: [] + diff --git a/apps/ai/config/ui.yaml b/apps/ai/config/ui.yaml new file mode 100644 index 0000000000..460be4e6f7 --- /dev/null +++ b/apps/ai/config/ui.yaml @@ -0,0 +1,2 @@ +footer: + defaultText: footer default text haha diff --git a/apps/ai/env/.env.dev b/apps/ai/env/.env.dev new file mode 100644 index 0000000000..3f14376d41 --- /dev/null +++ b/apps/ai/env/.env.dev @@ -0,0 +1,3 @@ +DB_HOST=localhost +DB_NAME=scow_server +AUTH_EXTERNAL_URL="http://auth:5000" diff --git a/apps/ai/launch.sh b/apps/ai/launch.sh new file mode 100755 index 0000000000..f99e565e2b --- /dev/null +++ b/apps/ai/launch.sh @@ -0,0 +1,24 @@ +BACKUP_DIR=".next.backup" + +if [ -d "$BACKUP_DIR" ]; then + rm -rf .next + cp -r .next.backup .next +else + cp -r .next .next.backup +fi + +BASE_PATH=$NEXT_PUBLIC_BASE_PATH + +# If BASE_PATH == "/", change it to "" +if [ "$BASE_PATH" = "/" ]; then + BASE_PATH="" +fi + +find .next \ + -type f \ + -exec sed -i \ + -e "s#/@BASE_PATH@/#${BASE_PATH}/#g" \ + -e "s#/@BASE_PATH@#${BASE_PATH}#g" \ + {} + + +pnpm serve:next diff --git a/apps/ai/next.config.mjs b/apps/ai/next.config.mjs new file mode 100644 index 0000000000..1475cfe509 --- /dev/null +++ b/apps/ai/next.config.mjs @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import os from "os"; +import { join } from "path"; + +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || "/"; + +const building = process.env.BUILDING === "1"; + +export default async () => { + + global.__CONFIG__ = { + BASE_PATH, + }; + + if (!building) { + // HACK setup ws proxy + setTimeout(() => { + const url = `http://localhost:${process.env.PORT || 3000}${join(BASE_PATH, "/api/setup")}`; + console.log("Calling setup url to initialize proxy and shell server", url); + + fetch(url).then(async (res) => { + console.log("Call completed. Response: ", await res.text()); + }).catch((e) => { + console.error("Error when calling proxy url to initialize ws proxy server", e); + }); + }); + + } + + /** @type {import('next').NextConfig} */ + const nextConfig = { + compiler: { + styledComponents: true, + }, + experimental: { + serverMinification: false, + }, + basePath: BASE_PATH === "/" ? undefined : BASE_PATH, + assetPrefix: BASE_PATH === "/" ? undefined : BASE_PATH, + webpack: (config) => { + config.resolve.extensionAlias = { + ".js": [".ts", ".tsx", ".js"], + ".jsx": [".ts", ".tsx", ".js"], + }; + config.module.rules.push({ + test: /\.node$/, + use: [ + { + loader: "nextjs-node-loader", + options: { + flags: os.constants.dlopen.RTLD_NOW, + outputPath: config.output.path, + }, + }, + ], + }); + + // config.plugins.forEach((i) => { + // if (i instanceof webpack.DefinePlugin) { + // if (i.definitions["process.env.__NEXT_ROUTER_BASEPATH"]) { + // i.definitions["process.env.__NEXT_ROUTER_BASEPATH"] = + // "(typeof window === \"undefined\" ? global : window).__CONFIG__?.BASE_PATH"; + // } + // } + // }); + return config; + }, + compiler: { + styledComponents: true, + }, + skipTrailingSlashRedirect: true, + transpilePackages: ["antd", "@ant-design/icons"], + }; + + return nextConfig; +}; diff --git a/apps/ai/package.json b/apps/ai/package.json new file mode 100644 index 0000000000..438c325855 --- /dev/null +++ b/apps/ai/package.json @@ -0,0 +1,124 @@ +{ + "name": "@scow/ai", + "version": "0.1.0", + "private": true, + "files": [ + ".next", + "public", + "next.config.mjs", + "build", + "assets", + "tsconfig.json", + "tsconfig.server.json", + "start.mjs" + ], + "scripts": { + "dev": "cross-env NEXT_PUBLIC_USE_MOCK=1 next dev", + "dev:server": "cross-env NEXT_PUBLIC_USE_MOCK=0 next dev", + "serve": "node start.mjs", + "serve:next": "next start", + "build": "npm run build:next", + "build:next": "rimraf .next.backup && cross-env BUILDING=1 NEXT_PUBLIC_BASE_PATH=/@BASE_PATH@ next build", + "build:ts": "rimraf build && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json", + "orm": "dotenv -e env/.env.dev -- npx mikro-orm", + "ormCreate": "dotenv -e env/.env.dev -- npx mikro-orm migration:create", + "ormUp": "dotenv -e env/.env.dev -- npx mikro-orm migration:up" + }, + "mikro-orm": { + "useTsNode": true, + "configPaths": [ + "./src/server/mikro-orm.config.ts", + "./src/server/mikro-orm.config.js" + ] + }, + "dependencies": { + "@ant-design/cssinjs": "1.17.2", + "@ant-design/icons": "5.2.6", + "@ddadaal/tsgrpc-client": "0.17.7", + "@ddadaal/tsgrpc-common": "0.2.5", + "@grpc/grpc-js": "1.9.14", + "@mikro-orm/cli": "6.0.5", + "@mikro-orm/core": "6.0.5", + "@mikro-orm/migrations": "6.0.5", + "@mikro-orm/mysql": "6.0.5", + "@mikro-orm/seeder": "6.0.5", + "@scow/config": "workspace:*", + "@scow/lib-auth": "workspace:*", + "@scow/lib-config": "workspace:*", + "@scow/lib-decimal": "workspace:*", + "@scow/lib-operation-log": "workspace:*", + "@scow/lib-scheduler-adapter": "workspace:*", + "@scow/lib-ssh": "workspace:*", + "@scow/lib-web": "workspace:*", + "@scow/lib-server": "workspace:*", + "@scow/scheduler-adapter-protos": "workspace:*", + "@scow/utils": "workspace:*", + "@scow/rich-error-model": "workspace:*", + "@sinclair/typebox": "0.32.12", + "@tanstack/react-query": "4.36.1", + "@trpc/client": "10.45.0", + "@trpc/next": "10.45.0", + "@trpc/react-query": "10.45.0", + "@trpc/server": "10.45.0", + "antd": "5.11.2", + "dayjs": "1.11.9", + "dotenv": "16.3.1", + "http-proxy": "1.18.1", + "lodash": "4.17.21", + "long": "5.2.3", + "mime-types": "2.1.35", + "next": "14.1.0", + "next-compose-plugins": "2.2.1", + "next-connect": "1.0.0", + "nextjs-cors": "2.2.0", + "node-ssh": "13.1.0", + "nookies": "2.5.2", + "nprogress": "0.2.0", + "pino": "8.16.2", + "protobufjs": "7.2.5", + "react": "18.2.0", + "react-async": "10.0.1", + "react-dom": "18.2.0", + "react-is": "18.2.0", + "react-typed-i18n": "2.3.0", + "simstate": "3.0.1", + "styled-components": "6.1.8", + "superjson": "2.2.1", + "swagger-ui-react": "5.10.3", + "trpc-openapi": "1.2.0", + "ws": "8.16.0", + "xterm": "5.2.1", + "xterm-addon-fit": "0.7.0", + "zod": "3.22.4", + "shell-quote": "1.8.1", + "replace-in-file": "7.1.0" + }, + "devDependencies": { + "@next/bundle-analyzer": "14.1.0", + "@testing-library/jest-dom": "6.1.4", + "@testing-library/react": "14.1.2", + "@types/busboy": "1.5.3", + "@types/google-protobuf": "3.15.10", + "@types/http-proxy": "1.17.14", + "@types/mime-types": "2.1.4", + "@types/node": "20.9.2", + "@types/nprogress": "0.2.3", + "@types/react": "18.2.48", + "@types/react-dom": "18.2.15", + "@types/styled-components": "5.1.34", + "@types/swagger-ui-react": "4.18.3", + "@types/url-join": "4.0.3", + "@types/ws": "8.5.10", + "@types/shell-quote": "1.7.4", + "fs-extra": "11.2.0", + "jest-environment-jsdom": "29.7.0", + "nextjs-node-loader": "1.1.5-alpha.0", + "node-mocks-http": "1.13.0", + "postcss": "8.4.31", + "ts-log": "2.2.5", + "webpack": "5.89.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/apps/ai/src/app/(auth)/algorithm/AlgorithmTable.tsx b/apps/ai/src/app/(auth)/algorithm/AlgorithmTable.tsx new file mode 100644 index 0000000000..6b70c7d4d6 --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/AlgorithmTable.tsx @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PlusOutlined } from "@ant-design/icons"; +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Button, Form, Input, Modal, Select, Space, Table, TableColumnsType } from "antd"; +import { useCallback, useState } from "react"; +import { SingleClusterSelector } from "src/components/ClusterSelector"; +import { FilterFormContainer } from "src/components/FilterFormContainer"; +import { ModalButton } from "src/components/ModalLink"; +import { AlgorithmInterface, AlgorithmTypeText, Framework } from "src/models/Algorithm"; +import { Cluster } from "src/server/trpc/route/config"; +import { formatDateTime } from "src/utils/datetime"; +import { trpc } from "src/utils/trpc"; + +import { AlgorithmVersionList } from "./AlgorithmVersionList"; +import { CreateAndEditAlgorithmModal } from "./CreateAndEditAlgorithmModal"; +import { CreateAndEditVersionModal } from "./CreateAndEditVersionModal"; + +interface Props { + isPublic: boolean; + clusters: Cluster[]; +} + +const FilterType = { + ALL: "全部", + ...AlgorithmTypeText, +} as const; + +type FilterTypeKeys = keyof typeof FilterType; + +interface FilterForm { + framework?: FilterTypeKeys, + nameOrDesc?: string, + clusterId?: string, +} + +interface PageInfo { + page: number; + pageSize?: number; +} + +const CreateAlgorithmModalButton = +ModalButton(CreateAndEditAlgorithmModal, { type: "primary", icon: }); +const EditAlgorithmModalButton = +ModalButton(CreateAndEditAlgorithmModal, { type: "link" }); +const CreateVersionModalButton = ModalButton(CreateAndEditVersionModal, { type: "link" }); + +export const AlgorithmTable: React.FC = ({ isPublic, clusters }) => { + const [{ confirm }, confirmModalHolder] = Modal.useModal(); + const { message } = App.useApp(); + + const [query, setQuery] = useState(() => { + return { + nameOrDesc: undefined, + framework: undefined, + clusterId:undefined, + }; + }); + + const [form] = Form.useForm(); + const [pageInfo, setPageInfo] = useState({ page: 1, pageSize: 10 }); + + const { data, isFetching, refetch, error } = trpc.algorithm.getAlgorithms.useQuery( + { ...pageInfo, + framework:query.framework === "ALL" ? undefined : query.framework, + nameOrDesc:query.nameOrDesc, + clusterId:query.clusterId, + isPublic, + }); + if (error) { + message.error("找不到算法"); + } + + const deleteAlgorithmMutation = trpc.algorithm.deleteAlgorithm.useMutation({ + onSuccess() { + message.success("删除算法成功"); + refetch(); + }, + onError() { + message.error("删除算法失败"); + } }); + + const deleteAlgorithm = useCallback( + (id: number) => { + confirm({ + title: "删除算法", + onOk:async () => { + await deleteAlgorithmMutation.mutateAsync({ id }); + }, + }); + }, + [], + ); + + const getCurrentCluster = useCallback((clusterId: string) => { + return clusters.find((c) => c.id === clusterId); + }, [clusters]); + + const columns: TableColumnsType = [ + { dataIndex: "name", title: "名称" }, + { dataIndex: "clusterId", title: "集群", + render: (_, r) => + getI18nConfigCurrentText(getCurrentCluster(r.clusterId)?.name, undefined) ?? r.clusterId }, + { dataIndex: "framework", title: "算法框架", render:(framework: Framework) => AlgorithmTypeText[framework] }, + { dataIndex: "description", title: "算法描述" }, + { dataIndex: "versions", title: "版本数量", + render: (_, r) => { + return r.versions.length; + } }, + isPublic ? { dataIndex: "shareUser", title: "分享者", + render: (_, r) => { + return r.owner; + } } : {}, + { dataIndex: "createTime", title: "创建时间", + render:(createTime) => formatDateTime(createTime), + }, + ...!isPublic ? [{ dataIndex: "action", title: "操作", + render: (_: any, r: AlgorithmInterface) => { + return ( + <> + { refetch(); }} + algorithmId={r.id} + algorithmName={r.name} + cluster={getCurrentCluster(r.clusterId)} + > + 创建新版本 + + + 编辑 + + + + ); + }, + }] : [], + ]; + + return ( +
+ + + layout="inline" + form={form} + initialValues={query} + onFinish={async () => { + const { nameOrDesc } = await form.validateFields(); + setQuery({ ...query, nameOrDesc: nameOrDesc?.trim() }); + setPageInfo({ page: 1, pageSize: pageInfo.pageSize }); + }} + > + + { + setQuery({ ...query, clusterId:val.id }); + }} + /> + + + + + + + + + + + + {!isPublic && ( + + 添加 + + )} + + Object.keys(x).length)} + pagination={{ + current: pageInfo.page, + defaultPageSize: 10, + pageSize: pageInfo.pageSize, + showSizeChanger: true, + total: data?.count, + onChange: (page, pageSize) => setPageInfo({ page, pageSize }), + }} + expandable={{ + expandedRowRender: (record) => { + const cluster = getCurrentCluster(record.clusterId); + return cluster && ( + + ); + }, + }} + scroll={{ x: true }} + /> + {/* antd中modal组件 */} + {confirmModalHolder} + + ); +}; + diff --git a/apps/ai/src/app/(auth)/algorithm/AlgorithmVersionList.tsx b/apps/ai/src/app/(auth)/algorithm/AlgorithmVersionList.tsx new file mode 100644 index 0000000000..f840429d45 --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/AlgorithmVersionList.tsx @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { TRPCClientError } from "@trpc/client"; +import { App, Button, Modal, Table } from "antd"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useEffect } from "react"; +import { ModalButton } from "src/components/ModalLink"; +import { AlgorithmInterface } from "src/models/Algorithm"; +import { SharedStatus } from "src/models/common"; +import { Cluster } from "src/server/trpc/route/config"; +import { AppRouter } from "src/server/trpc/router"; +import { getSharedStatusText } from "src/utils/common"; +import { formatDateTime } from "src/utils/datetime"; +import { trpc } from "src/utils/trpc"; + +import { CopyPublicAlgorithmModal } from "./CopyPublicAlgorithmModal"; +import { CreateAndEditVersionModal } from "./CreateAndEditVersionModal"; + + +export interface Props { + isPublic?: boolean; + algorithms: AlgorithmInterface[]; + algorithmId: number; + algorithmName: string | undefined; + cluster: Cluster; +} + +const EditVersionModalButton = ModalButton(CreateAndEditVersionModal, { type: "link" }); +const CopyPublicAlgorithmModalButton = ModalButton(CopyPublicAlgorithmModal, { type: "link" }); + +export const AlgorithmVersionList: React.FC = ( + { isPublic, algorithms, algorithmId, algorithmName, cluster }, +) => { + const { message } = App.useApp(); + const [{ confirm }, confirmModalHolder] = Modal.useModal(); + const router = useRouter(); + + const { data: versionData, isFetching, refetch, error: versionError } = + trpc.algorithm.getAlgorithmVersions.useQuery({ algorithmId:algorithmId, isPublic }); + if (versionError) { + message.error("找不到算法版本"); + } + + useEffect(() => { + refetch(); + }, [algorithms]); + + const checkFileExist = trpc.file.checkFileExist.useMutation(); + + const shareMutation = trpc.algorithm.shareAlgorithmVersion.useMutation({ + onSuccess() { + refetch(); + message.success("提交分享请求"); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "FORBIDDEN") { + message.error("没有权限分享此版本"); + return; + } + + message.error("分享失败"); + }, + }); + + const unShareMutation = trpc.algorithm.unShareAlgorithmVersion.useMutation({ + onSuccess() { + refetch(); + message.success("提交取消分享"); + }, + onError(err) { + const { data } = err as TRPCClientError; + if (data?.code === "FORBIDDEN") { + message.error("没有权限取消分享此版本"); + return; + } + + message.error("取消分享失败"); + }, + }); + + const deleteAlgorithmVersionMutation = trpc.algorithm.deleteAlgorithmVersion.useMutation({ + onSuccess() { + message.success("删除算法版本成功"); + refetch(); + }, + onError() { + message.error("删除算法版本失败"); + } }); + + const deleteAlgorithmVersion = useCallback( + (id: number, isConfirmed?: boolean) => { + confirm({ + title: isConfirmed ? "源文件已被删除,是否删除本条数据" : "删除算法版本", + onOk:async () => { + await deleteAlgorithmVersionMutation.mutateAsync({ algorithmVersionId: id, algorithmId }); + }, + }); + }, + [algorithmId], + ); + + return ( + <> +
formatDateTime(createTime) }, + { dataIndex: "action", title: "操作", + ...isPublic ? {} : { width: 350 }, + render: (_, r) => { + return isPublic ? ( + + 复制 + + ) : + ( + <> + + 编辑 + + + + + + ); + + }, + }, + ]} + /> + {/* antd中modal组件 */} + {confirmModalHolder} + + ); +}; diff --git a/apps/ai/src/app/(auth)/algorithm/CopyPublicAlgorithmModal.tsx b/apps/ai/src/app/(auth)/algorithm/CopyPublicAlgorithmModal.tsx new file mode 100644 index 0000000000..a481038b8e --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/CopyPublicAlgorithmModal.tsx @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { AlgorithmVersionInterface } from "src/models/Algorithm"; +import { Cluster } from "src/server/trpc/route/config"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +export interface Props { + open: boolean; + data: AlgorithmVersionInterface; + algorithmId: number; + algorithmVersionId: number; + algorithmName: string | undefined; + cluster?: Cluster; + onClose: () => void; +} + +interface FormFields { + targetAlgorithmName: string; + versionName: string, + versionDescription?: string, + path: string, +} + +export const CopyPublicAlgorithmModal: React.FC = ( + { open, onClose, algorithmId, algorithmVersionId, algorithmName, cluster, data }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const copyMutation = trpc.algorithm.copyPublicAlgorithmVersion.useMutation({ + onSuccess() { + message.success("复制算法成功"); + onClose(); + }, + onError(err) { + const errCode = err.data?.code; + if (errCode === "CONFLICT") { + message.error("目标算法名称已存在"); + form.setFields([ + { + name: "targetDatasetName", + errors: ["目标算法名称已存在"], + }, + ]); + return; + } + + message.error(err.message); + + }, + }); + + const onOk = async () => { + const { targetAlgorithmName, versionName, versionDescription, path } = await form.validateFields(); + copyMutation.mutate({ + algorithmId, + algorithmVersionId, + algorithmName: targetAlgorithmName, + versionName, + versionDescription: versionDescription ?? "", + path, + }); + }; + + return ( + +
+ + {algorithmName} + + + + + + {getI18nConfigCurrentText(cluster?.name, undefined)} + + + + + + + + + { + form.setFields([{ name: "path", value: path, touched: true }]); + form.validateFields(["path"]); + }} + clusterId={cluster?.id ?? ""} + /> + ) + } + /> + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/algorithm/CreateAndEditAlgorithmModal.tsx b/apps/ai/src/app/(auth)/algorithm/CreateAndEditAlgorithmModal.tsx new file mode 100644 index 0000000000..e34dbb2ff1 --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/CreateAndEditAlgorithmModal.tsx @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal, Select } from "antd"; +import React from "react"; +import { SingleClusterSelector } from "src/components/ClusterSelector"; +import { AlgorithmTypeText, Framework } from "src/models/Algorithm"; +import { Cluster } from "src/server/trpc/route/config"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +interface EditProps { + cluster?: Cluster; + algorithmName: string; + algorithmId: number; + algorithmFramework: Framework; + algorithmDescription?: string; +} +export interface Props { + open: boolean; + onClose: () => void; + refetch: () => void; + editData?: EditProps; +} +type AlgorithmType = keyof typeof AlgorithmTypeText; + +interface FormFields { + name: string, + type: AlgorithmType, + cluster: Cluster, + description?: string, +} + +export const CreateAndEditAlgorithmModal: React.FC = ( + { open, onClose, refetch, editData }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const createAlgorithmMutation = trpc.algorithm.createAlgorithm.useMutation({ + onSuccess() { + message.success("添加算法成功"); + form.resetFields(); + refetch(); + onClose(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("算法名称已存在"); + form.setFields([ + { + name: "name", + errors: ["算法名称已存在"], + }, + ]); + } else { + message.error("添加算法失败,请联系管理员"); + } + } }); + + const updateAlgorithmMutation = trpc.algorithm.updateAlgorithm.useMutation({ + onSuccess() { + message.success("修改算法成功"); + refetch(); + onClose(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("算法名称已存在"); + form.setFields([ + { + name: "name", + errors: ["算法名称已存在"], + }, + ]); + } else if (e.data?.code === "NOT_FOUND") { + message.error("算法未找到"); + } else if (e.data?.code === "PRECONDITION_FAILED") { + message.error("有正在分享或正在取消分享的数据存在,请稍后再试"); + } else { + message.error("修改算法失败"); + } + } }); + + const onOk = async () => { + const { name, type, description, cluster } = await form.validateFields(); + + if (editData?.algorithmName) { + updateAlgorithmMutation.mutate({ + id:editData.algorithmId, name, framework:type, description, + }); + } else { + createAlgorithmMutation.mutate({ + name, framework:type, description, clusterId:cluster.id, + }); + } + }; + + return ( + +
+ + + + {editData?.cluster ? ( + + {getI18nConfigCurrentText(editData?.cluster?.name, undefined)} + + ) : ( + + + + )} + + + + + + + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/algorithm/CreateAndEditVersionModal.tsx b/apps/ai/src/app/(auth)/algorithm/CreateAndEditVersionModal.tsx new file mode 100644 index 0000000000..ab5a6e623a --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/CreateAndEditVersionModal.tsx @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { Cluster } from "src/server/trpc/route/config"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +interface EditProps { + versionName?: string; + versionId?: number; + versionDescription?: string; +} +export interface Props { + open: boolean; + onClose: () => void; + algorithmId: number; + algorithmName: string | undefined; + cluster?: Cluster; + refetch: () => void; + editData?: EditProps; +} + +interface FormFields { + versionName: string, + versionDescription?: string, + path: string, +} + +export const CreateAndEditVersionModal: React.FC = ( + { open, onClose, algorithmId, algorithmName, refetch, cluster, editData }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const createAlgorithmVersionMutation = trpc.algorithm.createAlgorithmVersion.useMutation({ + onSuccess() { + message.success("创建新版本成功"); + form.resetFields(); + onClose(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("版本名称已存在"); + form.setFields([ + { + name: "versionName", + errors: ["版本名称已存在"], + }, + ]); + return; + } else if (e.data?.code === "BAD_REQUEST") { + message.error("所选文件夹路径不存在"); + form.setFields([ + { + name: "name", + errors: ["所选文件夹路径不存在"], + }, + ]); + } else { + message.error(e.message); + } + }, + }); + + + const updateAlgorithmVersionMutation = trpc.algorithm.updateAlgorithmVersion.useMutation({ + onSuccess() { + message.success("修改版本成功"); + onClose(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("版本名称已存在"); + form.setFields([ + { + name: "versionName", + errors: ["版本名称已存在"], + }, + ]); + } + else if (e.data?.code === "NOT_FOUND") { + message.error("算法或算法版本未找到"); + } + else if (e.data?.code === "PRECONDITION_FAILED") { + message.error("有正在分享或正在取消分享的数据存在,请稍后再试"); + } + else { + message.error("修改版本失败"); + } + }, + }); + + const onOk = async () => { + form.validateFields(); + const { versionName, versionDescription, path } = await form.validateFields(); + if (editData?.versionName && editData.versionId) { + updateAlgorithmVersionMutation.mutate({ + algorithmVersionId:editData.versionId, + versionName, + versionDescription, + algorithmId, + }); + } + else { + createAlgorithmVersionMutation.mutate({ + versionName, + versionDescription, + path, + algorithmId, + }); + } + }; + + return ( + +
+ + {algorithmName} + + + {getI18nConfigCurrentText(cluster?.name, undefined)} + + + + + + + + { + !editData?.versionName ? ( + + { + form.setFields([{ name: "path", value: path, touched: true }]); + form.validateFields(["path"]); + }} + clusterId={cluster?.id ?? ""} + /> + ) + } + /> + + ) : undefined + } + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/algorithm/page.tsx b/apps/ai/src/app/(auth)/algorithm/page.tsx new file mode 100644 index 0000000000..40a7288791 --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/page.tsx @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/algorithm/private"); +} diff --git a/apps/ai/src/app/(auth)/algorithm/private/page.tsx b/apps/ai/src/app/(auth)/algorithm/private/page.tsx new file mode 100644 index 0000000000..fe54bd452e --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/private/page.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { AlgorithmTable } from "src/app/(auth)/algorithm/AlgorithmTable"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { PageTitle } from "src/components/PageTitle"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/algorithm/public/page.tsx b/apps/ai/src/app/(auth)/algorithm/public/page.tsx new file mode 100644 index 0000000000..5fbaa42d7b --- /dev/null +++ b/apps/ai/src/app/(auth)/algorithm/public/page.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { AlgorithmTable } from "src/app/(auth)/algorithm/AlgorithmTable"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { PageTitle } from "src/components/PageTitle"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/context.ts b/apps/ai/src/app/(auth)/context.ts new file mode 100644 index 0000000000..d1d96093e6 --- /dev/null +++ b/apps/ai/src/app/(auth)/context.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + + +import React, { useContext } from "react"; +import { ClientUserInfo } from "src/server/trpc/route/auth"; +import { Cluster, PublicConfig } from "src/server/trpc/route/config"; + + +export const PublicConfigContext = React.createContext<{ + publicConfig: PublicConfig, + clusters: Cluster[], + user: ClientUserInfo; + defaultClusterContext: { + defaultCluster: Cluster; + setDefaultCluster: (cluster: Cluster) => void; + removeDefaultCluster: () => void; + } + }>(undefined!); + +export const usePublicConfig = () => { + return useContext(PublicConfigContext); +}; diff --git a/apps/ai/src/app/(auth)/dashboard/page.tsx b/apps/ai/src/app/(auth)/dashboard/page.tsx new file mode 100644 index 0000000000..d12ddc2d5b --- /dev/null +++ b/apps/ai/src/app/(auth)/dashboard/page.tsx @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { join } from "path"; +import { Head } from "src/utils/head"; +import { trpc } from "src/utils/trpc"; +import { styled } from "styled-components"; + +const Logo = styled.div` + position: relative; + width: 100%; + display: flex; + justify-content: center; +`; + +export default function Page() { + + const { data } = trpc.config.publicConfig.useQuery(); + + return ( +
+ + { + data ? ( + + logo + + ) : undefined + } +
+ ); +} + diff --git a/apps/ai/src/app/(auth)/dataset/CopyPublicDatasetModal.tsx b/apps/ai/src/app/(auth)/dataset/CopyPublicDatasetModal.tsx new file mode 100644 index 0000000000..70adaf241f --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/CopyPublicDatasetModal.tsx @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { useUser } from "src/app/auth"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { Cluster } from "src/server/trpc/route/config"; +import { DatasetVersionInterface } from "src/server/trpc/route/dataset/datasetVersion"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +export interface Props { + open: boolean; + onClose: () => void; + datasetId: number; + datasetName: string | undefined; + datasetVersionId: number; + data: DatasetVersionInterface; + cluster?: Cluster; +} + +interface FormFields { + versionName: string, + versionDescription?: string, + targetPath: string, + targetDatasetName: string; +} + +export const CopyPublicDatasetModal: React.FC = ( + { open, onClose, data, datasetId, datasetName, cluster }, +) => { + + const [form] = Form.useForm(); + const { message } = App.useApp(); + const user = useUser(); + + const copyMutation = trpc.dataset.copyPublicDatasetVersion.useMutation({ + onSuccess() { + message.success("复制数据集成功"); + onClose(); + form.resetFields(); + }, + onError(err) { + if (err.data?.code === "CONFLICT") { + form.setFields([ + { + name: "targetDatasetName", + errors: ["目标数据集名称已存在"], + }, + ]); + return; + } + + message.error(err.message); + }, + }); + + const onOk = async () => { + const { targetPath, targetDatasetName, versionName, versionDescription } = await form.validateFields(); + copyMutation.mutate({ + datasetId, + datasetName: targetDatasetName, + path: targetPath, + datasetVersionId: data.id, + versionName, + versionDescription: versionDescription ?? "", + }); + }; + + return ( + +
+ + {datasetName} + + + + + + {getI18nConfigCurrentText(cluster?.name, undefined)} + + + + + + + + + { + form.setFields([{ name: "targetPath", value: path, touched: true }]); + form.validateFields(["targetPath"]); + }} + clusterId={cluster?.id ?? ""} + /> + ) + } + /> + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/dataset/CreateEditDSVersionModal.tsx b/apps/ai/src/app/(auth)/dataset/CreateEditDSVersionModal.tsx new file mode 100644 index 0000000000..628bf3835c --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/CreateEditDSVersionModal.tsx @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { Cluster } from "src/server/trpc/route/config"; +import { DatasetVersionInterface } from "src/server/trpc/route/dataset/datasetVersion"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +export interface Props { + open: boolean; + onClose: () => void; + datasetId: number; + datasetName: string | undefined; + isEdit?: boolean; + editData?: DatasetVersionInterface; + cluster?: Cluster; + refetch: () => void; +} + +interface FormFields { + versionName: string, + versionDescription?: string, + path: string, +} + +export const CreateEditDSVersionModal: React.FC = ( + { open, onClose, datasetId, datasetName, isEdit, editData, cluster, refetch }, +) => { + + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const createMutation = trpc.dataset.createDatasetVersion.useMutation({ + onSuccess() { + message.success("创建新版本成功"); + onClose(); + form.resetFields(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("版本名称已存在"); + form.setFields([ + { + name: "versionName", + errors: ["版本名称已存在"], + }, + ]); + } else if (e.data?.code === "BAD_REQUEST") { + message.error("所选文件夹路径不存在"); + form.setFields([ + { + name: "path", + errors: ["所选文件夹路径不存在"], + }, + ]); + } else { + message.error(e.message); + } + }, + }); + + const editMutation = trpc.dataset.updateDatasetVersion.useMutation({ + onSuccess() { + message.success("编辑版本成功"); + onClose(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("版本名称已存在"); + form.setFields([ + { + name: "versionName", + errors: ["版本名称已存在"], + }, + ]); + } else if (e.data?.code === "NOT_FOUND") { + message.error("无法找到数据集或数据集版本"); + } else if (e.data?.code === "PRECONDITION_FAILED") { + message.error("有正在分享或正在取消分享的数据存在,请稍后再试"); + } else { + message.success("编辑版本失败"); + } + }, + }); + + const onOk = async () => { + form.validateFields(); + const { versionName, versionDescription, path } = await form.validateFields(); + isEdit && editData ? editMutation.mutate({ + datasetVersionId: editData.id, + versionName, + versionDescription, + datasetId: editData.datasetId, + }) + : createMutation.mutate({ + versionName, + versionDescription, + path, + datasetId, + }); + }; + + return ( + +
+ + {datasetName} + + + {getI18nConfigCurrentText(cluster?.name, undefined)} + + + + + + + + { + !isEdit && ( + <> + + { + form.setFields([{ name: "path", value: path, touched: true }]); + form.validateFields(["path"]); + }} + clusterId={cluster?.id ?? ""} + /> + ) + } + /> + + + ) + } + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/dataset/CreateEditDatasetModal.tsx b/apps/ai/src/app/(auth)/dataset/CreateEditDatasetModal.tsx new file mode 100644 index 0000000000..5edaf2ae21 --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/CreateEditDatasetModal.tsx @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal, Select } from "antd"; +import React, { useEffect } from "react"; +import { SingleClusterSelector } from "src/components/ClusterSelector"; +import { DatasetType, DatasetTypeText, SceneType, SceneTypeText } from "src/models/Dateset"; +import { Cluster } from "src/server/trpc/route/config"; +import { DatasetInterface } from "src/server/trpc/route/dataset/dataset"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +import { defaultClusterContext } from "../defaultClusterContext"; + +export interface Props { + open: boolean; + onClose: () => void; + refetch: () => void; + isEdit: boolean; + editData?: DatasetInterface; + clusters: Cluster[]; +} + +interface FormFields { + id?: number | undefined, + name: string, + cluster: Cluster, + type: string, + scene: string, + description?: string, +} + +export const CreateEditDatasetModal: React.FC = ( + { open, onClose, refetch, isEdit, editData, clusters }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const { defaultCluster } = defaultClusterContext(clusters); + + useEffect(() => { + resetForm(); + }, []); + + const resetForm = () => { + isEdit && editData ? + form.setFieldsValue({ + type: editData.type, + scene: editData.scene, + }) : form.setFieldsValue({ + type: DatasetType.IMAGE, + scene: SceneType.CWS, + }); + }; + + const createMutation = trpc.dataset.createDataset.useMutation({ + onSuccess() { + message.success("添加数据集成功"); + onClose(); + form.resetFields(); + resetForm(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("数据集名称已存在"); + form.setFields([ + { + name: "name", + errors: ["数据集名称已存在"], + }, + ]); + return; + } + + message.error("添加数据集失败"); + }, + }); + + const editMutation = trpc.dataset.updateDataset.useMutation({ + onSuccess() { + message.success("编辑数据集成功"); + onClose(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("数据集名称已存在"); + form.setFields([ + { + name: "name", + errors: ["数据集名称已存在"], + }, + ]); + } + else if (e.data?.code === "NOT_FOUND") { + message.error("无法找到数据集"); + } + else if (e.data?.code === "PRECONDITION_FAILED") { + message.error("有正在分享或正在取消分享的数据存在,请稍后再试"); + } + else { + message.error("编辑数据集失败"); + } + }, + }); + + const onOk = async () => { + const { name, type, description, scene, cluster } = await form.validateFields(); + (isEdit && editData) ? editMutation.mutate({ + id: editData.id, + name, + type, + scene, + description, + }) + : createMutation.mutate({ + name, + clusterId: cluster.id, + type, + description, + scene, + }); + }; + + return ( + +
+ + + + {isEdit && editData ? ( + + {getI18nConfigCurrentText( + clusters.find((x) => (x.id === editData.clusterId))?.name, undefined) + ?? editData.clusterId } + + ) : ( + + + + ) + } + + ({ label:value, value:key }))} + /> + + + + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/dataset/DatasetListTable.tsx b/apps/ai/src/app/(auth)/dataset/DatasetListTable.tsx new file mode 100644 index 0000000000..b206213d25 --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/DatasetListTable.tsx @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PlusOutlined } from "@ant-design/icons"; +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { TRPCClientError } from "@trpc/client"; +import { App, Button, Form, Input, Modal, Select, Space, Table } from "antd"; +import { useCallback, useState } from "react"; +import { SingleClusterSelector } from "src/components/ClusterSelector"; +import { FilterFormContainer } from "src/components/FilterFormContainer"; +import { ModalButton } from "src/components/ModalLink"; +import { DatasetTypeText, SceneTypeText } from "src/models/Dateset"; +import { Cluster } from "src/server/trpc/route/config"; +import { DatasetInterface } from "src/server/trpc/route/dataset/dataset"; +import { AppRouter } from "src/server/trpc/router"; +import { formatDateTime } from "src/utils/datetime"; +import { trpc } from "src/utils/trpc"; + +import { defaultClusterContext } from "../defaultClusterContext"; +import { CreateEditDatasetModal } from "./CreateEditDatasetModal"; +import { CreateEditDSVersionModal } from "./CreateEditDSVersionModal"; +import { DatasetVersionList } from "./DatasetVersionList"; + +interface Props { + isPublic: boolean; + clusters: Cluster[]; +} + +const FilterType = { + ALL: "全部", + ...DatasetTypeText, +} as { [key: string]: string }; + +type FilterTypeKeys = Extract; + +interface FilterForm { + cluster?: Cluster | undefined, + type?: FilterTypeKeys | undefined, + nameOrDesc?: string | undefined, +} + +interface PageInfo { + page: number; + pageSize?: number; +} + +const CreateDatasetModalButton = ModalButton(CreateEditDatasetModal, { type: "primary", icon: }); +const EditDatasetModalButton = ModalButton(CreateEditDatasetModal, { type: "link" }); +const CreateEditVersionModalButton = ModalButton(CreateEditDSVersionModal, { type: "link" }); + +export const DatasetListTable: React.FC = ({ isPublic, clusters }) => { + + const [{ confirm }, confirmModalHolder] = Modal.useModal(); + + const { message } = App.useApp(); + + const { defaultCluster } = defaultClusterContext(clusters); + + const [query, setQuery] = useState(() => { + return { + cluster: defaultCluster, + nameOrDesc: undefined, + type: undefined, + }; + }); + + const [form] = Form.useForm(); + const [pageInfo, setPageInfo] = useState({ page: 1, pageSize: 10 }); + + const { data, refetch, isFetching, error } = trpc.dataset.list.useQuery({ + ...pageInfo, ...query, clusterId: query.cluster?.id, isPublic, + }); + if (error) { + message.error("找不到数据集"); + } + + const deleteDatasetMutation = trpc.dataset.deleteDataset.useMutation({ + onSuccess() { + refetch(); + message.success("删除成功"); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "NOT_FOUND") { + message.error("找不到数据集"); + } else { + message.error("删除数据集失败"); + } + }, + }); + + const deleteDataset = useCallback( + (id: number) => { + confirm({ + title: "删除数据集", + onOk: async () => { + await deleteDatasetMutation.mutateAsync({ id }); + }, + }); + }, + [], + ); + + const getCurrentCluster = useCallback((clusterId: string | undefined) => { + if (clusterId) { + return clusters.find((c) => c.id === clusterId); + } + }, [clusters]); + + return ( +
+ + + layout="inline" + form={form} + initialValues={query} + onFinish={async () => { + const { nameOrDesc } = await form.validateFields(); + setQuery({ ...query, nameOrDesc: nameOrDesc?.trim() }); + setPageInfo({ page: 1, pageSize: pageInfo.pageSize }); + refetch(); + }} + > + + { setQuery({ ...query, cluster: value }); }} + /> + + + + + + + + + {!isPublic && ( + + + 添加 + + + )} + +
+ getI18nConfigCurrentText(getCurrentCluster(r.clusterId)?.name, undefined) ?? r.clusterId }, + { dataIndex: "type", title: "数据集类型", + render: (_, r) => DatasetTypeText[r.type] }, + { dataIndex: "description", title: "数据集描述" }, + { dataIndex: "scene", title: "应用场景", + render: (_, r) => SceneTypeText[r.scene] }, + { dataIndex: "versions", title: "版本数量", + render: (_, r) => r.versions.length }, + isPublic ? { dataIndex: "shareUser", title: "分享者", + render: (_, r) => r.owner } : {}, + { dataIndex: "createTime", title: "创建时间", + render: (_, r) => r.createTime ? formatDateTime(r.createTime) : "-" }, + ...!isPublic ? [{ dataIndex: "action", title: "操作", + render: (_: any, r: DatasetInterface) => { + return ( + <> + { + refetch(); + }} + > + 创建新版本 + + + 编辑 + + + + ); + }, + }] : [], + ]} + pagination={setPageInfo ? { + current: pageInfo.page, + defaultPageSize: 10, + pageSize: pageInfo.pageSize, + showSizeChanger: true, + total: data?.count, + onChange: (page, pageSize) => setPageInfo({ page, pageSize }), + } : false} + expandable={{ + expandedRowRender: (record) => { + const cluster = getCurrentCluster(record.clusterId); + return cluster && ( + + ); + }, + }} + scroll={{ x: true }} + /> + {/* antd中modal组件 */} + {confirmModalHolder} + + ); +}; + diff --git a/apps/ai/src/app/(auth)/dataset/DatasetVersionList.tsx b/apps/ai/src/app/(auth)/dataset/DatasetVersionList.tsx new file mode 100644 index 0000000000..9ec646fe31 --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/DatasetVersionList.tsx @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { TRPCClientError } from "@trpc/client"; +import { App, Button, Table } from "antd"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useEffect } from "react"; +import { ModalButton } from "src/components/ModalLink"; +import { SharedStatus } from "src/models/common"; +import { Cluster } from "src/server/trpc/route/config"; +import { DatasetInterface } from "src/server/trpc/route/dataset/dataset"; +import { AppRouter } from "src/server/trpc/router"; +import { getSharedStatusText } from "src/utils/common"; +import { formatDateTime } from "src/utils/datetime"; +import { trpc } from "src/utils/trpc"; + +import { CopyPublicDatasetModal } from "./CopyPublicDatasetModal"; +import { CreateEditDSVersionModal } from "./CreateEditDSVersionModal"; + +export interface Props { + datasets: DatasetInterface[]; + datasetId: number; + datasetName: string; + isPublic?: boolean; + cluster: Cluster; +} + +const CopyPublicDatasetModalButton = ModalButton(CopyPublicDatasetModal, { type: "link" }); + +export const DatasetVersionList: React.FC = ( + { datasets, datasetId, datasetName, isPublic, cluster }, +) => { + const { modal, message } = App.useApp(); + const CreateEditVersionModalButton = ModalButton(CreateEditDSVersionModal, { type: "link" }); + + const router = useRouter(); + + const { data: versionData, isFetching, refetch, error: versionError } + = trpc.dataset.versionList.useQuery({ datasetId, isPublic }); + if (versionError) { + message.error("找不到数据集版本"); + } + + useEffect(() => { + refetch(); + }, [datasets]); + + const checkFileExist = trpc.file.checkFileExist.useMutation(); + + const shareMutation = trpc.dataset.shareDatasetVersion.useMutation({ + onSuccess() { + refetch(); + message.success("提交分享请求"); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "FORBIDDEN") { + message.error("没有权限分享此数据集版本"); + } else { + message.error("分享数据集版本失败"); + } + }, + }); + + const unShareMutation = trpc.dataset.unShareDatasetVersion.useMutation({ + onSuccess() { + refetch(); + message.success("提交取消分享请求"); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "FORBIDDEN") { + message.error("没有权限取消分享此数据集版本"); + } else { + message.error("取消分享数据集版本失败"); + } + }, + }); + + const deleteMutation = trpc.dataset.deleteDatasetVersion.useMutation({ + onSuccess() { + message.success("删除成功"); + refetch(); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "NOT_FOUND") { + message.error("找不到该数据集版本"); + } else { + message.error("删除数据集版本失败"); + } + }, + }); + + const deleteDatasetVersion = useCallback( + (id: number, datasetId: number, isConfirmed?: boolean) => { + modal.confirm({ + title: isConfirmed ? "源文件已被删除,是否删除本条数据" : "删除数据集版本", + onOk: async () => { + await deleteMutation.mutateAsync({ + datasetVersionId: id, + datasetId, + }); + }, + }); + }, + [], + ); + + return ( +
r.createTime ? formatDateTime(r.createTime) : "-" }, + { dataIndex: "action", title: "操作", + render: (_, r) => { + return !isPublic ? ( + <> + + 编辑 + + + + + + ) : ( + + 复制 + + ); + }, + }, + ]} + /> + ); +}; + + diff --git a/apps/ai/src/app/(auth)/dataset/page.tsx b/apps/ai/src/app/(auth)/dataset/page.tsx new file mode 100644 index 0000000000..d0c9a6446f --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/page.tsx @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/dataset/private"); +} diff --git a/apps/ai/src/app/(auth)/dataset/private/page.tsx b/apps/ai/src/app/(auth)/dataset/private/page.tsx new file mode 100644 index 0000000000..118c1b1bd7 --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/private/page.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { usePublicConfig } from "src/app/(auth)/context"; +import { PageTitle } from "src/components/PageTitle"; + +import { DatasetListTable } from "../DatasetListTable"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/dataset/public/page.tsx b/apps/ai/src/app/(auth)/dataset/public/page.tsx new file mode 100644 index 0000000000..d576d66965 --- /dev/null +++ b/apps/ai/src/app/(auth)/dataset/public/page.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PageTitle } from "src/components/PageTitle"; + +import { usePublicConfig } from "../../context"; +import { DatasetListTable } from "../DatasetListTable"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/defaultClusterContext.ts b/apps/ai/src/app/(auth)/defaultClusterContext.ts new file mode 100644 index 0000000000..5cbfe59908 --- /dev/null +++ b/apps/ai/src/app/(auth)/defaultClusterContext.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { Cluster } from "src/server/trpc/route/config"; + + +const SCOW_DEFAULT_CLUSTER_ID = "SCOW_DEFAULT_CLUSTER_ID"; + +export function defaultClusterContext(clusters: Cluster[]) { + + const clusterId = window.localStorage.getItem(SCOW_DEFAULT_CLUSTER_ID); + const defaultCluster = clusters.find((cluster) => cluster.id === clusterId) || clusters[0]; + + const setDefaultCluster = (cluster: Cluster) => { + window.localStorage.setItem(SCOW_DEFAULT_CLUSTER_ID, cluster.id); + }; + + const removeDefaultCluster = () => { + if (typeof window !== "undefined") { + window.localStorage.removeItem(SCOW_DEFAULT_CLUSTER_ID); + } + }; + + return { defaultCluster, setDefaultCluster, removeDefaultCluster }; + +} diff --git a/apps/ai/src/app/(auth)/files/CreateFileModal.tsx b/apps/ai/src/app/(auth)/files/CreateFileModal.tsx new file mode 100644 index 0000000000..11d4c70c67 --- /dev/null +++ b/apps/ai/src/app/(auth)/files/CreateFileModal.tsx @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { App, Form, Input, Modal } from "antd"; +import { join } from "path"; +import { Cluster } from "src/server/trpc/route/config"; +import { trpc } from "src/utils/trpc"; + +interface Props { + open: boolean; + onClose: () => void; + reload: () => void; + cluster: Cluster; + path: string; +} + +interface FormProps { + newFileName: string; +} + +export const CreateFileModal: React.FC = ({ open, onClose, path, reload, cluster }) => { + + const { message } = App.useApp(); + + const [form] = Form.useForm(); + + const mutation = trpc.file.createFile.useMutation({ + onSuccess: () => { + message.success("创建成功"); + reload(); + onClose(); + form.resetFields(); + }, + onError: (e) => { + if (e.data?.code === "CONFLICT") { + message.error("同名文件已经存在"); + } else { + throw e; + } + }, + }); + + const onSubmit = async () => { + const { newFileName } = await form.validateFields(); + + mutation.mutate({ path: join(path, newFileName), clusterId: cluster.id }); + }; + + return ( + +
+ + {path} + + label="文件名" name="newFileName" rules={[{ required: true }]}> + + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/files/FileManager.tsx b/apps/ai/src/app/(auth)/files/FileManager.tsx new file mode 100644 index 0000000000..08a6b38aa8 --- /dev/null +++ b/apps/ai/src/app/(auth)/files/FileManager.tsx @@ -0,0 +1,485 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { CopyOutlined, DatabaseOutlined, DeleteOutlined, EyeInvisibleOutlined, EyeOutlined, + FileAddOutlined, FolderAddOutlined, HomeOutlined, LeftOutlined, MacCommandOutlined, + RightOutlined, ScissorOutlined, SnippetsOutlined, UploadOutlined, + UpOutlined } from "@ant-design/icons"; +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import type { inferRouterOutputs } from "@trpc/server"; +import { App, Button, Divider, Modal, Space } from "antd"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { join } from "path"; +import React, { useEffect, useRef, useState } from "react"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { useOperation } from "src/app/(auth)/files/[cluster]/context"; +import { FilterFormContainer } from "src/components/FilterFormContainer"; +import { MkdirModal } from "src/components/MkdirModal"; +import { ModalButton, ModalLink } from "src/components/ModalLink"; +import { TitleText } from "src/components/PageTitle"; +import { TableTitle } from "src/components/TableTitle"; +import { UploadModal } from "src/components/UploadModal"; +import { Cluster } from "src/server/trpc/route/config"; +import { AppRouter } from "src/server/trpc/router"; +import { trpc } from "src/utils/trpc"; +import { styled } from "styled-components"; + +import { urlToDownload } from "./api"; +import { CreateFileModal } from "./CreateFileModal"; +import { FileTable } from "./FileTable.jsx"; +import { PathBar } from "./PathBar"; +import { RenameModal } from "./RenameModal"; + +interface Props { + cluster: Cluster; + loginNodes: Record; + path: string; + urlPrefix: string; +} + +const TopBar = styled(FilterFormContainer)` + display: flex; + flex-direction: row; + padding-bottom: 8px; + + &>button { + margin: 0px 4px; + } +`; + +const OperationBar = styled(TableTitle)` + justify-content: space-between; + flex-wrap: wrap; + gap: 4px; +`; + +type FileInfoKey = React.Key; + +type FileInfo = inferRouterOutputs["file"]["listDirectory"][0]; + +const fileInfoKey = (f: FileInfo, path: string): string => join(path, f.name); + +interface PromiseSettledResult { + status: string; + value?: FileInfo | undefined; +} + +const operationTexts = { + copy: "复制", + move: "移动", +}; + +export const FileManager: React.FC = ({ cluster, loginNodes, path, urlPrefix }) => { + + const { message, modal } = App.useApp(); + const router = useRouter(); + const { publicConfig } = usePublicConfig(); + + const prevPathRef = useRef(path); + + const [selectedKeys, setSelectedKeys] = useState([]); + const { operation, setOperation } = useOperation(); + const [showHiddenFile, setShowHiddenFile] = useState(false); + + const filesQuery = trpc.file.listDirectory.useQuery({ + clusterId: cluster.id, path, + }, { enabled: path !== "~" }); + + const loginNodeAddress = loginNodes[cluster.id]; + + const reload = filesQuery.refetch; + + const fullUrl = (path: string) => join(urlPrefix, cluster.id, path); + + const up = () => { + const paths = path.split("/"); + + const newPath = paths.length === 1 + ? path : "/" + paths.slice(0, paths.length - 1).join("/"); + router.replace(fullUrl(newPath)); + }; + + const toHome = () => { + router.push(fullUrl("~")); + }; + + const back = () => { + router.back(); + }; + + const forward = () => { + history.forward(); + }; + + useEffect(() => { + if (path === "~") { + return; + } + + setSelectedKeys([]); + + reload() + .then(() => { prevPathRef.current = path; }) + .catch(() => { + if (prevPathRef.current !== path) { + router.push(fullUrl(prevPathRef.current)); + } + }); + }, [path]); + + const resetSelectedAndOperation = () => { + setSelectedKeys([]); + setOperation(undefined); + }; + + + const copyOrMoveMutation = trpc.file.copyOrMove.useMutation({ + onError(error) { + const operationText = operationTexts[operation!.op]; + + if (error.data?.code === "CONFLICT") { + Modal.error({ + title: `${operationText}失败`, + content: "存在相同的目录或文件", + }); + return; + } + + Modal.error({ + title: `${operationText}失败`, + content: error.message, + }); + }, + onSettled() { + resetSelectedAndOperation(); + reload(); + }, + }); + + const paste = async () => { + if (!operation) { return; } + const operationText = operationTexts[operation.op]; + + setOperation({ ...operation, started: true }); + + // if only one file is selected, show detailed error information + if (operation.selected.length === 1) { + const filename = operation.selected[0].name; + const fromPath = join(operation.originalPath, filename); + + copyOrMoveMutation.mutate({ + op: operation.op, + clusterId: cluster.id, fromPath, toPath: join(path, filename), + }); + + return; + } + + await Promise.allSettled(operation.selected.map(async (x) => { + return await copyOrMoveMutation.mutateAsync({ + op: operation.op, + clusterId: cluster.id, + fromPath: join(operation.originalPath, x.name), + toPath: join(path, x.name), + }).then(() => { + setOperation((o) => o ? { ...operation, completed: o.completed.concat(x) } : undefined); + return x; + }).catch(() => { + return undefined; + }); + })) + .then((successfulInfo) => { + const successfulCount = successfulInfo.filter((x) => x).length; + const allCount = operation.selected.length; + if (successfulCount === allCount) { + message.success(`${operationText}${allCount}项成功!`); + resetSelectedAndOperation(); + } else { + message.error(`${operationText}成功${successfulCount}项,失败${allCount - successfulCount}项`); + } + }).catch((e) => { + console.log(e); + message.error(`执行${operationText}操作时遇到错误`); + }).finally(() => { + resetSelectedAndOperation(); + reload(); + }); + + }; + + const deleteMutation = trpc.file.deleteItem.useMutation(); + + const onDeleteClick = () => { + const files = keysToFiles(selectedKeys); + modal.confirm({ + title: "确认删除", + content: `确认要删除选中的${files.length}项?`, + onOk: async () => { + await Promise.allSettled(files.map(async (x: FileInfo) => { + return deleteMutation.mutateAsync({ + target: x.type, + clusterId: cluster.id, + path: join(path, x.name), + }).then(() => x).catch(() => undefined); + })) + .then((successfulInfo) => { + const failedCount = successfulInfo.filter((x: PromiseSettledResult) => + (!x || x.status === "rejected" || !x.value)).length; + const allCount = files.length; + if (failedCount === 0) { + message.success(`删除${allCount}项成功!`); + resetSelectedAndOperation(); + } else { + message.error(`删除成功${allCount - failedCount}项,失败${failedCount}项`); + setOperation((o) => o && ({ ...o, started: false })); + } + }).catch((e) => { + console.log(e); + message.error("执行删除操作时遇到错误"); + setOperation((o) => o && ({ ...o, started: false })); + setSelectedKeys([]); + }).finally(() => { + setOperation(undefined); + reload(); + }); + }, + }); + + }; + + const keysToFiles = (keys: React.Key[]) => { + return filesQuery.data?.filter((x: FileInfo) => keys.includes(fileInfoKey(x, path))) ?? []; + }; + + const onHiddenClick = () => { + setShowHiddenFile(!showHiddenFile); + }; + + return ( +
+ + + 集群{getI18nConfigCurrentText(cluster.name, undefined)}文件管理 + + + + + + + + { + operation ? ( + operation.started ? ( + + {`正在${operationTexts[operation.op]},` + + `已完成:${operation.completed.length} / ${operation.selected.length}`} + + ) : ( + + {`已选择${operationTexts[operation.op]}${operation.selected.length}个项`} + setOperation(undefined)} style={{ marginLeft: "4px" }}> + 取消 + + + )) : "" + } + + + + + + + + 新文件 + + reload()} + > + 新目录 + + + + files.filter((file) => showHiddenFile || !file.name.startsWith("."))} + loading={filesQuery.isFetching} + rowSelection={{ + selectedRowKeys: selectedKeys, + onChange: setSelectedKeys, + }} + rowKey={(r) => fileInfoKey(r, path)} + onRow={(r) => ({ + onClick: () => { + setSelectedKeys([fileInfoKey(r, path)]); + }, + onDoubleClick: () => { + if (r.type === "DIR") { + router.push(fullUrl(join(path, r.name))); + } else if (r.type === "FILE") { + const href = urlToDownload(cluster.id, join(path, r.name), false, publicConfig.BASE_PATH); + openPreviewLink(href); + } + }, + })} + fileNameRender={(_, r) => ( + r.type === "DIR" ? ( + + {r.name} + + ) : ( + { + const href = urlToDownload(cluster.id, join(path, r.name), false, publicConfig.BASE_PATH); + openPreviewLink(href); + }} + > + {r.name} + + ) + )} + actionRender={(_, i: FileInfo) => ( + + { + i.type === "FILE" ? ( + + 下载 + + ) : undefined + } + + 重命名 + + { + const fullPath = join(path, i.name); + modal.confirm({ + title: "确认删除", + // icon: < />, + content: `确认删除${fullPath}?`, + okText: "确认", + onOk: () => { + deleteMutation.mutate({ + target: i.type, clusterId: cluster.id, path: fullPath, + }, { + onSuccess: () => { + message.success("删除成功!"); + resetSelectedAndOperation(); + reload(); + }, + }); + }, + }); + }} + > + 删除 + + + )} + /> +
+ ); +}; + +const RenameLink = ModalLink(RenameModal); +const CreateFileButton = ModalButton(CreateFileModal, { icon: }); +const MkdirButton = ModalButton(MkdirModal, { icon: }); +const UploadButton = ModalButton(UploadModal, { icon: }); + + +function openPreviewLink(href: string) { + window.open(href, "ViewFile", "location=yes,resizable=yes,scrollbars=yes,status=yes"); +} diff --git a/apps/ai/src/app/(auth)/files/FileTable.tsx b/apps/ai/src/app/(auth)/files/FileTable.tsx new file mode 100644 index 0000000000..a2b258a7a9 --- /dev/null +++ b/apps/ai/src/app/(auth)/files/FileTable.tsx @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { FileOutlined, FolderOutlined } from "@ant-design/icons"; +import { Table, TableProps, Tooltip } from "antd"; +import { ColumnsType } from "antd/es/table"; +import React from "react"; +import { TableFileInfo } from "src/app/(auth)/files/[cluster]/context"; +import { FileType } from "src/server/trpc/route/file"; +import { compareDateTime, formatDateTime } from "src/utils/datetime"; +import { formatSize } from "src/utils/format"; +import { compareNumber } from "src/utils/math"; + +type ColumnKey = ("type" | "name" | "mtime" | "size" | "mode" | "action"); + +const nodeModeToString = (mode: number) => { + const numberPermission = (mode & parseInt("777", 8)).toString(8); + + const toStr = (char: string) => { + const num = +char; + return ((num & 4) !== 0 ? "r" : "-") + ((num & 2) !== 0 ? "w" : "-") + ((num & 1) !== 0 ? "x" : "-"); + }; + + return [0, 1, 2].reduce((prev, curr) => prev + toStr(numberPermission[curr]), ""); +}; + +interface Props extends TableProps { + files: TableFileInfo[]; + filesFilter?: (files: TableFileInfo[]) => TableFileInfo[]; + fileNameRender?: (fileName: string, r: TableFileInfo) => React.ReactNode; + actionRender?: (_: any, r: TableFileInfo) => React.ReactNode; + hiddenColumns?: ColumnKey[]; +} + +const fileTypeIcons = { + "FILE": FileOutlined, + "DIR": FolderOutlined, +} as Record; + +export const FileTable: React.FC = ( + { + files, + fileNameRender, + actionRender, + filesFilter, + hiddenColumns, + ...otherProps + }, +) => { + + const columns: ColumnsType = [ + { + key: "type", + dataIndex: "type", + title: "", + width: "32px", + render: (_, r) => React.createElement(fileTypeIcons[r.type]), + }, + { + key: "name", + dataIndex: "name", + title: "文件名", + defaultSortOrder: "ascend", + sorter: (a, b) => a.type.localeCompare(b.type) === 0 + ? a.name.localeCompare(b.name) + : a.type.localeCompare(b.type), + sortDirections: ["ascend", "descend"], + render: fileNameRender, + }, + { + key: "mtime", + dataIndex: "mtime", + title: "修改日期", + render: (mtime: string | undefined) => mtime ? formatDateTime(mtime) : "", + sorter: (a, b) => a.type.localeCompare(b.type) === 0 + ? compareDateTime(a.mtime, b.mtime) === 0 + ? a.name.localeCompare(b.name) + : compareDateTime(a.mtime, b.mtime) + : a.type.localeCompare(b.type), + }, + { + key: "size", + dataIndex: "size", + title: "大小", + render: (size: number | undefined, file: TableFileInfo) => (size === undefined || file.type === "DIR") + ? "" + : ( + + {formatSize(Math.round(size / 1024))} + + ), + sorter: (a, b) => { + return a.type.localeCompare(b.type) === 0 + ? compareNumber(a.size, b.size) === 0 + ? a.name.localeCompare(b.name) + : compareNumber(a.size, b.size) + : a.type.localeCompare(b.type); + }, + }, + { + key: "mode", + dataIndex: "mode", + title: "权限", + render: (mode: number | undefined) => mode === undefined ? "" : nodeModeToString(mode), + }, + ...(actionRender ? [{ + key: "action", + dataIndex: "action", + title: "操作", + render: actionRender, + }] : []), + ]; + + return ( +
column.key ? !hiddenColumns.includes(column.key as ColumnKey) : true) + : columns + } + pagination={false} + size="small" + /> + ); +}; diff --git a/apps/ai/src/app/(auth)/files/PathBar.tsx b/apps/ai/src/app/(auth)/files/PathBar.tsx new file mode 100644 index 0000000000..ccf82c0289 --- /dev/null +++ b/apps/ai/src/app/(auth)/files/PathBar.tsx @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { ReloadOutlined, RightOutlined } from "@ant-design/icons"; +import { Breadcrumb, Button, Input } from "antd"; +import { useEffect, useState } from "react"; +import { styled } from "styled-components"; + +interface Props { + path: string; + loading: boolean; + onPathChange: (path: string) => void; + breadcrumbItemRender: (pathSegment: string, index: number, path: string) => React.ReactNode; + prefix?: React.ReactNode +} + +const Bar = styled.div` + display: flex; + width: 100%; +`; + +const BarStateBar = styled(Bar)` + border: 1px solid ${({ theme }) => theme.token.colorBorder}; + border-radius: ${({ theme }) => theme.token.borderRadius}px; + padding: 0 8px; + margin: 0 4px; + + .ant-breadcrumb { + align-self: center; + } +`; + +export const PathBar: React.FC = ({ + path, + loading, + onPathChange, + breadcrumbItemRender, + prefix, +}) => { + + const [state, setState] = useState<"bar" | "input">("bar"); + + const [input, setInput] = useState(path); + + useEffect(() => { + setInput(path); + }, [path]); + + const pathSegments = path === "/" ? [""] : path.split("/"); + + const icon = path === input + ? + : ; + + return ( + { + setInput(path); + setState("bar"); + }} + > + {state === "input" + ? ( + { + setInput(e.target.value); + }} + onSearch={onPathChange} + enterButton={icon} + autoFocus + prefix={prefix} + /> + ) : ( + <> + setState("input")}> + + {pathSegments.map((segment, index) => ( + + {breadcrumbItemRender(segment, index, pathSegments.slice(1, index + 1).join("/"))} + + ))} + + + + + + {!isPublic && ( + + 添加 + + + )} + +
+ getI18nConfigCurrentText(clusters.find((x) => (x.id === r.clusterId))?.name, undefined) ?? r.clusterId }, + { dataIndex: "tag", title: "标签" }, + { dataIndex: "source", title: "镜像来源", + render: (_, r) => SourceText[r.source] }, + { dataIndex: "description", title: "镜像描述" }, + isPublic ? { dataIndex: "shareUser", title: "分享者", + render: (_, r) => r.owner } : {}, + { dataIndex: "status", title: "状态", + render: (_, r) => { + switch (r.status) { + case Status.CREATING: + return 创建中; + case Status.CREATED: + return 已创建; + default: + return 创建失败; + } + }, + }, + { dataIndex: "createTime", title: "创建时间", + render: (_, r) => r.createTime ? formatDateTime(r.createTime) : "-" }, + { dataIndex: "action", title: "操作", + render: (_, r) => { + const shareOrUnshareStr = r.isShared ? "取消分享" : "分享"; + return !isPublic ? + ( + <> + { r.status === Status.CREATED && ( + + )} + + {/* { r.source === Source.INTERNAL && ( + }> + + + )} */} + + { r.status === Status.CREATED && ( + + 编辑 + + )} + + + ) : + ( + + 复制 + + ); + }, + }, + ]} + pagination={setPageInfo ? { + current: pageInfo.page, + defaultPageSize: 10, + pageSize: pageInfo.pageSize, + showSizeChanger: true, + total: data?.count, + onChange: (page, pageSize) => setPageInfo({ page, pageSize }), + } : false} + scroll={{ x: true }} + /> + + ); +}; + diff --git a/apps/ai/src/app/(auth)/image/page.tsx b/apps/ai/src/app/(auth)/image/page.tsx new file mode 100644 index 0000000000..7d020142c7 --- /dev/null +++ b/apps/ai/src/app/(auth)/image/page.tsx @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/image/private"); +} diff --git a/apps/ai/src/app/(auth)/image/private/page.tsx b/apps/ai/src/app/(auth)/image/private/page.tsx new file mode 100644 index 0000000000..8d94e2eef3 --- /dev/null +++ b/apps/ai/src/app/(auth)/image/private/page.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PageTitle } from "src/components/PageTitle"; + +import { usePublicConfig } from "../../context"; +import { ImageListTable } from "../ImageListTable"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/image/public/page.tsx b/apps/ai/src/app/(auth)/image/public/page.tsx new file mode 100644 index 0000000000..afa832f67a --- /dev/null +++ b/apps/ai/src/app/(auth)/image/public/page.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PageTitle } from "src/components/PageTitle"; + +import { usePublicConfig } from "../../context"; +import { ImageListTable } from "../ImageListTable"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/AppSessionsTable.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/AppSessionsTable.tsx new file mode 100644 index 0000000000..8341a53513 --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/AppSessionsTable.tsx @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { ExclamationCircleOutlined } from "@ant-design/icons"; +import { App, Button, Checkbox, Form, Input, Popconfirm, Space, Table, TableColumnsType, Tooltip } from "antd"; +import { useRouter } from "next/navigation"; +import { join } from "path"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { FilterFormContainer } from "src/components/FilterFormContainer"; +import { ModalButton } from "src/components/ModalLink"; +import { JobType } from "src/models/Job"; +import { Cluster } from "src/server/trpc/route/config"; +import { AppSession } from "src/server/trpc/route/jobs/apps"; +import { calculateAppRemainingTime, compareDateTime, formatDateTime } from "src/utils/datetime"; +import { compareNumber } from "src/utils/math"; +import { trpc } from "src/utils/trpc"; + +import { ConnectTopAppLink } from "./ConnectToAppLink"; +import { SaveImageModal } from "./SaveImageModal"; + +interface FilterForm { + appJobName: string | undefined + } + +export enum AppTableStatus { + UNFINISHED = "UNFINISHED", + FINISHED = "FINISHED" +} + +interface Props { + cluster: Cluster + status: AppTableStatus +} + +export function compareState(a: string, b: string): -1 | 0 | 1 { + const endState = "ENDED"; + if (a === b || (a !== endState && b !== endState)) { return 0; } + if (a === endState) { return -1; } + return 1; +} + +const SaveImageModalButton = ModalButton(SaveImageModal, { type: "link" }); + +export const AppSessionsTable: React.FC = ({ cluster, status }) => { + + const router = useRouter(); + const { message } = App.useApp(); + + const unfinished = status === AppTableStatus.UNFINISHED; + + const [query, setQuery] = useState(() => { + return { appJobName: undefined }; + }); + const [form] = Form.useForm(); + + const [checked, setChecked] = useState(true); + const [connectivityRefreshToken, setConnectivityRefreshToken] = useState(false); + + const { data, refetch, isLoading, isFetching } = trpc.jobs.listAppSessions.useQuery({ + clusterId: cluster.id, isRunning: unfinished, + }); + + const cancelJobMutation = trpc.jobs.cancelJob.useMutation({ + onError:(e) => { + message.error(`操作失败: ${e.message}`); + }, + onSuccess: () => { + refetch(); + }, + }); + + const columns: TableColumnsType = [ + { + title: "作业ID", + dataIndex: "jobId", + width: "8%", + sorter: (a, b) => compareNumber(a.jobId, b.jobId), + }, + { + title: "作业名", + dataIndex: "sessionId", + width: "25%", + ellipsis: true, + }, + { + title: "类型", + dataIndex: "jobType", + width: "10%", + render: (_, record) => { + if (record.jobType === JobType.APP) { + return "应用"; + } + return "训练"; + }, + }, + { + title: "应用", + dataIndex: "appId", + render: (appId: string, record) => record.appName ?? appId, + sorter: (a, b) => (!a.submitTime || !b.submitTime) ? -1 : compareDateTime(a.submitTime, b.submitTime), + }, + { + title: "提交时间", + dataIndex: "submitTime", + width: "15%", + render: (_, record) => record.submitTime ? formatDateTime(record.submitTime) : "", + }, + { + title: "状态", + dataIndex: "state", + width: "12%", + render: (_, record) => ( + record.reason ? ( + + + {record.state} + + + + ) : ( + {record.state} + ) + ), + sorter: (a, b) => compareState (a.state, b.state) + ? compareState (a.state, b.state) : + compareNumber(a.jobId, b.jobId), + defaultSortOrder: "descend", + + }, + ...(unfinished ? [{ + title: "剩余时间", + dataIndex: "remainingTime", + }, + ] : []), + { + title: "操作", + key: "action", + fixed:"right", + width: "10%", + render: (_, record) => ( + + { + (record.state === "RUNNING") ? ( + <> + {record.jobType === JobType.APP && ( + + )} + { + await cancelJobMutation.mutateAsync({ + cluster: cluster.id, + jobId: record.jobId, + }); + message.success("任务结束请求已经提交"); + } + } + > + 结束 + + + ) : undefined + } + { + (record.state === "PENDING" || record.state === "SUSPENDED") ? ( + { + await cancelJobMutation.mutateAsync({ + cluster: cluster.id, + jobId: record.jobId, + }); + message.success("任务取消请求已经提交"); + } + } + > + 取消 + + ) : undefined + } + { + (record.state === "RUNNING" && record.jobType === JobType.APP) ? ( + 保存镜像 + ) : undefined + } + { + router.push(join("/files", cluster.id, record.dataPath)); + }} + > + 进入目录 + + + ), + }, + ]; + + + const reloadTable = useCallback(() => { + refetch(); + setConnectivityRefreshToken((f) => !f); + }, [setConnectivityRefreshToken]); + + useEffect(() => { + if (checked && unfinished) { + const interval = setInterval(() => { + reloadTable(); + }, 10000); + return () => clearInterval(interval); + } + }, [checked, unfinished]); + + const filteredData = useMemo(() => { + if (!data) { return []; } + return data.sessions.filter((x) => { + if (query.appJobName) { + return x.sessionId.toLowerCase().includes(query.appJobName!.toLowerCase()); + } + return true; + }).map((x) => ({ + ...x, + remainingTime: x.state === "RUNNING" ? calculateAppRemainingTime(x.runningTime, x.timeLimit) : + x.state === "PENDING" ? "" : x.timeLimit, + })); + }, [data, query]); + + return ( + <> + + + layout="inline" + form={form} + initialValues={query} + onFinish={async () => { + setQuery({ + ...(await form.validateFields()), + }); + }} + > + + + + + + + + + + + + + + {unfinished && ( + + { setChecked(e.target.checked); }} + > + 10s自动刷新 + + + ) } + + +
record.sessionId} + loading={isLoading || isFetching} + scroll={{ x: filteredData?.length ? 1200 : true }} + pagination={{ + showSizeChanger: true, + defaultPageSize: 50, + }} + /> + + ); +}; diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/ConnectToAppLink.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/ConnectToAppLink.tsx new file mode 100644 index 0000000000..4bea100b5e --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/ConnectToAppLink.tsx @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { parsePlaceholder } from "@scow/lib-config/build/parse"; +import { App } from "antd"; +import { join } from "path"; +import { useEffect } from "react"; +import { DisabledA } from "src/components/DisabledA"; +import { AppSession } from "src/server/trpc/route/jobs/apps"; +import { trpc } from "src/utils/trpc"; + +import { usePublicConfig } from "../../context"; + +export interface Props { + session: AppSession; + cluster: string; + refreshToken: boolean; +} + +export const ConnectTopAppLink: React.FC = ({ + session, cluster, refreshToken, +}) => { + const { publicConfig: { BASE_PATH } } = usePublicConfig(); + const { message } = App.useApp(); + + const { data, refetch } = trpc.jobs.checkAppConnectivity.useQuery({ clusterId: cluster, jobId: session.jobId }, { + enabled: !!session.jobId, + }); + + const connectMutation = trpc.jobs.connectToApp.useMutation( + { + onError(e) { + message.error(`连接应用失败: ${e.message}`); + }, + }, + ); + + useEffect(() => { + refetch(); + }, [refreshToken]); + + + const onClick = async () => { + + const reply = await connectMutation.mutateAsync({ + cluster, + sessionId: session.sessionId, + }); + + if (reply.type === "web") { + const { connect, host, password, port, proxyType } = reply; + const interpolatedValues = { HOST: host, PASSWORD: password, PORT: port }; + const path = parsePlaceholder(connect.path, interpolatedValues); + + const interpolateValues = (obj: Record): Record => { + return Object.keys(obj).reduce>((prev, curr) => { + prev[curr] = parsePlaceholder(obj[curr], interpolatedValues); + return prev; + }, {}); + }; + + const query = connect.query ? interpolateValues(connect.query) : {}; + const formData = connect.formData ? interpolateValues(connect.formData) : undefined; + + const pathname = join(BASE_PATH, "/api/proxy", cluster, proxyType, host, String(port), path); + + const url = pathname + "?" + new URLSearchParams(query).toString(); + + if (connect.method === "GET") { + window.open(url, "_blank"); + } else { + const form = document.createElement("form"); + form.style.display = "none"; + form.action = url; + form.method = "POST"; + form.target = "_blank"; + if (formData) { + Object.keys(formData).forEach((k) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = k; + input.value = formData[k]; + form.appendChild(input); + }); + } + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + } + + } else { + // TODO: vnc app + return; + } + + }; + + return ( + 连接 + ); +}; diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/LaunchAppForm.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/LaunchAppForm.tsx new file mode 100644 index 0000000000..130e1b555e --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/LaunchAppForm.tsx @@ -0,0 +1,715 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { I18nStringType } from "@scow/config/build/i18n"; +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Button, Col, Divider, Form, Input, InputNumber, Row, Select, Space, Spin } from "antd"; +import { Rule } from "antd/es/form"; +import dayjs from "dayjs"; +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import { AccountSelector } from "src/components/AccountSelector"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { AlgorithmInterface, AlgorithmVersionInterface } from "src/models/Algorithm"; +import { ModelInterface, ModelVersionInterface } from "src/models/Model"; +import { DatasetInterface } from "src/server/trpc/route/dataset/dataset"; +import { DatasetVersionInterface } from "src/server/trpc/route/dataset/datasetVersion"; +import { AppCustomAttribute } from "src/server/trpc/route/jobs/apps"; +import { formatSize } from "src/utils/format"; +import { trpc } from "src/utils/trpc"; + +import { useDataOptions, useDataVersionOptions } from "./hooks"; + +interface Props { + appId?: string; + appName?: string; + appImage?: { + name: string; + tag: string; + }; + attributes?: AppCustomAttribute[]; + appComment?: I18nStringType; + clusterId: string; + clusterInfo: ClusterConfig; + isTraining?: boolean; +} + +interface FixedFormFields { + appJobName: string; + algorithm: { name: number, version: number }; + image: { name: number }; + startCommand?: string; + dataset: { name: number, version: number }; + model: { name: number, version: number }; + mountPoint: string | undefined; + partition: string | undefined; + coreCount: number; + gpuCount: number | undefined; + account: string; + maxTime: number; + command?: string; +} + +interface CustomFormFields { + customFields: {[key: string]: number | string | undefined}; +} +type FormFields = CustomFormFields & FixedFormFields; + +interface ClusterConfig { + partitions: Partition[]; + schedulerName: string, +} + +interface Partition { + name: string; + memMb: number; + cores: number; + gpus: number; + nodes: number; + comment?: string; +} + +export enum AccessiblityType { + PUBLIC = "PUBLIC", + PRIVATE = "PRIVATE", +} + +// 生成默认应用名称,命名规则为"当前应用名-年月日-时分秒" +const genAppJobName = (appName: string): string => { + return `${appName}-${dayjs().format("YYYYMMDD-HHmmss")}`; +}; + +const initialValues = { + coreCount: 1, + gpuCount: 1, + maxTime: 60, +} as Partial; + +const inputNumberFloorConfig = { + formatter: (value: number | undefined) => `${Math.floor(value ?? 0)}`, + parser: (value: string | undefined) => Math.floor(value ? +value : 0), +}; + + +export const LaunchAppForm = (props: Props) => { + + const { clusterId, appName, clusterInfo, isTraining = false, appId, attributes = []} = props; + + const { message } = App.useApp(); + + const router = useRouter(); + + const [form] = Form.useForm(); + + const [currentPartitionInfo, setCurrentPartitionInfo] = useState(); + + const { dataOptions: datasetOptions, isDataLoading: isDatasetsLoading } = useDataOptions( + form, + "dataset", + trpc.dataset.list.useQuery, + clusterId, + (dataset) => ({ label: `${dataset.name}(${dataset.owner})`, value: dataset.id }), + ); + + const { dataVersionOptions: datasetVersionOptions, isDataVersionsLoading: isDatasetVersionsLoading } = + useDataVersionOptions( + form, + "dataset", + trpc.dataset.versionList.useQuery, + (x) => ({ label: x.versionName, value: x.id }), + ); + + const { dataOptions: algorithmOptions, isDataLoading: isAlgorithmLoading } = useDataOptions( + form, + "algorithm", + trpc.algorithm.getAlgorithms.useQuery, + clusterId, + (x) => ({ label:`${x.name}(${x.owner})`, value: x.id }), + ); + + const { dataVersionOptions: algorithmVersionOptions, isDataVersionsLoading: isAlgorithmVersionsLoading } = + useDataVersionOptions( + form, + "algorithm", + trpc.algorithm.getAlgorithmVersions.useQuery, + (x) => ({ label: x.versionName, value: x.id }), + ); + + const { dataOptions: modelOptions, isDataLoading: isModelsLoading } = useDataOptions( + form, + "model", + trpc.model.list.useQuery, + clusterId, + (x) => ({ label: `${x.name}(${x.owner})`, value: x.id }), + ); + + const { dataVersionOptions: modelVersionOptions, isDataVersionsLoading: isModelVersionsLoading } = + useDataVersionOptions( + form, + "model", + trpc.model.versionList.useQuery, + (x) => ({ label: x.versionName, value: x.id }), + ); + + const imageType = Form.useWatch(["image", "type"], form); + const selectedImage = Form.useWatch(["image", "name"], form); + + const isImagePublic = imageType !== undefined ? imageType === AccessiblityType.PUBLIC : imageType; + + const { data: images, isLoading: isImagesLoading } = trpc.image.list.useQuery({ + isPublic: isImagePublic, + clusterId, + withExternal: true, + }, { + enabled: isImagePublic !== undefined, + }); + + const imageOptions = useMemo(() => { + return images?.items.map((x) => ({ label: `${x.name}:${x.tag}`, value: x.id })); + }, [images]); + + // 暂时写死为1 + const nodeCount = 1; + + const coreCount = Form.useWatch("coreCount", form) as number; + + const gpuCount = Form.useWatch("gpuCount", form) as number; + + const memorySize = (currentPartitionInfo ? + currentPartitionInfo.gpus ? nodeCount * gpuCount + * Math.floor(currentPartitionInfo.cores / currentPartitionInfo.gpus) + * Math.floor(currentPartitionInfo.memMb / currentPartitionInfo.cores) : + nodeCount * coreCount * Math.floor(currentPartitionInfo.memMb / currentPartitionInfo.cores) : 0); + const memoryDisplay = formatSize(memorySize, ["MB", "GB", "TB"]); + + const coreCountSum = currentPartitionInfo?.gpus + ? nodeCount * gpuCount * Math.floor(currentPartitionInfo.cores / currentPartitionInfo.gpus) + : nodeCount * coreCount; + + + const handlePartitionChange = (partition: string) => { + const partitionInfo = clusterInfo + ? clusterInfo.partitions.find((x) => x.name === partition) + : undefined; + if (!!partitionInfo?.gpus) { + form.setFieldValue("gpuCount", 1); + } else { + form.setFieldValue("coreCount", 1); + } + setCurrentPartitionInfo(partitionInfo); + + }; + + const customFormItems = useMemo(() => attributes.map((item, index) => { + const rules: Rule[] = item.type === "NUMBER" + ? [{ type: "integer" }, { required: item.required }] + : [{ required: item.required }]; + + const placeholder = item.placeholder ?? ""; + + // 筛选选项:若没有配置requireGpu直接使用,配置了requireGpu项使用与否则看改分区有无GPU + const selectOptions = item.select.filter((x) => !x.requireGpu || (x.requireGpu && currentPartitionInfo?.gpus)); + const initialValue = item.type === "SELECT" ? (item.defaultValue ?? selectOptions[0].value) : item.defaultValue; + + let inputItem: JSX.Element; + + // 特殊处理某些应用的工作目录需要使用文件选择器 + if (item.name === "workingDir") { + inputItem = ( + { + form.setFieldsValue({ + customFields: { + [item.name]: path, + }, + }); + form.validateFields([["customFields", item.name]]); + }} + clusterId={clusterId ?? ""} + /> + ) + } + /> + ); + } else { + inputItem = item.type === "NUMBER" ? + () + : item.type === "TEXT" ? () + : ( + + + + + + { + form.setFieldValue("startCommand", undefined); + }} + loading={isImagesLoading && isImagePublic !== undefined} + options={imageOptions} + /> + + + + {(selectedImage && !isTraining) ? ( + + + + ) : null } + + + + { + form.setFieldValue(["algorithm", "version"], undefined); + }} + loading={isAlgorithmLoading} + options={algorithmOptions} + /> + + + { + form.setFieldsValue({ dataset: { name: undefined, version: undefined } }); + }} + options={ + [ + { + value: AccessiblityType.PRIVATE, + label: "我的数据集", + }, + { + value: AccessiblityType.PUBLIC, + label: "公共数据集", + }, + + ] + } + /> + + + + + + + + + + { + form.setFieldValue(["model", "version"], undefined); + }} + loading={isModelsLoading } + options={modelOptions} + /> + + + { + form.setFieldValue("mountPoint", path); + form.validateFields(["mountPoint"]); + }} + clusterId={clusterId ?? ""} + /> + ) + } + /> + + 资源 + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/SelectAppTable.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/SelectAppTable.tsx new file mode 100644 index 0000000000..c79273a30a --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/SelectAppTable.tsx @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PictureOutlined } from "@ant-design/icons"; +import { Avatar, Card, Col, Result, Row, Tooltip } from "antd"; +import Link from "next/link"; +import { join } from "path"; +import { useState } from "react"; +import { AppSchema } from "src/server/trpc/route/jobs/apps"; +import { styled } from "styled-components"; + + +const CardContainer = styled.div` + flex: 1; + display: flex; + flex-wrap: wrap; +`; + +const AvatarContainer = styled.div` + display: flex; + justify-content: center; +`; + +const NameContainer = styled.div` + text-align: center; + margin-top: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + + +interface Props { + publicPath: string + clusterId: string + apps: AppSchema[], + isTraining?: boolean, +} + +interface ImageErrorMap { + [appId: string]: boolean; +} + + +export const SelectAppTable: React.FC = ({ publicPath, clusterId, apps, isTraining = false }) => { + + const [imageErrorMap, setImageErrorMap] = useState({}); + + const handleImageError = (appId: string) => { + setImageErrorMap((prevMap) => ({ ...prevMap, [appId]: true })); + }; + + if (Object.keys(apps || {}).length === 0) { + return ( + + ); + } + + return ( + + + {apps.map((app) => ( + + + + + + { + (app.logoPath && imageErrorMap[app.id] !== true) ? ( + handleImageError(app.id)} + /> + ) : ( + } + /> + ) + } + + {app.name} + + + + + ))} + + + + ); +}; diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/[appId]/page.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/[appId]/page.tsx new file mode 100644 index 0000000000..de4ff220aa --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/[appId]/page.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { LoadingOutlined } from "@ant-design/icons"; +import { PageTitle } from "src/components/PageTitle"; +import { trpc } from "src/utils/trpc"; + +import { LaunchAppForm } from "../../LaunchAppForm"; + + +export default function Page({ params }: {params: {clusterId: string, appId: string}}) { + + const { appId, clusterId } = params; + + const { data: appInfo, isLoading: isAppLoading } = trpc.jobs.getAppMetadata.useQuery({ clusterId, appId }); + + const { data: clusterInfo, isLoading: isClusterLoading } = trpc.config.getClusterConfig.useQuery({ clusterId }); + + + if (isAppLoading || isClusterLoading || !appInfo || !clusterInfo) { + return ; + } + + return ( +
+ + +
+ ); +} + + diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/page.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/page.tsx new file mode 100644 index 0000000000..76f0b9c119 --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/createApps/page.tsx @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { usePublicConfig } from "src/app/(auth)/context"; +import { PageTitle } from "src/components/PageTitle"; +import { NotFoundPage } from "src/layouts/error/NotFoundPage"; +import { ServerErrorPage } from "src/layouts/error/ServerErrorPage"; +import { trpc } from "src/utils/trpc"; + +import { SelectAppTable } from "../SelectAppTable"; + + +const useClusterAppConfigQuery = (clusterId: string) => { + return trpc.jobs.listAvailableApps.useQuery({ clusterId }); +}; + +export default function Page({ params }: {params: {clusterId: string}}) { + const { clusterId } = params; + + const { publicConfig } = usePublicConfig(); + const cluster = publicConfig.CLUSTERS.find((x) => x.id === clusterId); + + + if (!cluster) { + return ; + } + + const { data, isLoading, isError } = useClusterAppConfigQuery(clusterId); + + if (isLoading) { + return

loading...

; + } + + if (isError) { + return ( + + ); + } + + return ( + <> + + + + ); +} diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/historyJobs/page.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/historyJobs/page.tsx new file mode 100644 index 0000000000..fc82b3db05 --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/historyJobs/page.tsx @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { usePublicConfig } from "src/app/(auth)/context"; +import { PageTitle } from "src/components/PageTitle"; +import { NotFoundPage } from "src/layouts/error/NotFoundPage"; + +import { AppSessionsTable, AppTableStatus } from "../AppSessionsTable"; + +export default function Page({ params }: {params: {clusterId: string}}) { + const { clusterId } = params; + + const { publicConfig } = usePublicConfig(); + const cluster = publicConfig.CLUSTERS.find((x) => x.id === clusterId); + + + if (!cluster) { + return ; + } + + return ( + <> + + + + ); +} diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/hooks.ts b/apps/ai/src/app/(auth)/jobs/[clusterId]/hooks.ts new file mode 100644 index 0000000000..cddfc08a2f --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/hooks.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { UseQueryResult } from "@tanstack/react-query"; +import { Form, FormInstance } from "antd"; +import { useMemo } from "react"; + +import { AccessiblityType } from "./LaunchAppForm"; + +interface Option { + label: string; + value: number; +} + +type DataType = "dataset" | "algorithm" | "model" + +interface QueryHookFunction { + (args: TQueryFnData, options?: any): UseQueryResult; +} + +export function useDataOptions( + form: FormInstance, + dataType: DataType, + queryHook: QueryHookFunction, + clusterId: string, + mapItemToOption: (item: T) => Option, +): { dataOptions: Option[], isDataLoading: boolean } { + const typePath = [dataType, "type"]; + const itemType = Form.useWatch(typePath, form); + const isItemPublic = itemType !== undefined ? itemType === AccessiblityType.PUBLIC : itemType; + + const { data: items, isLoading: isDataLoading } = queryHook({ + isPublic : isItemPublic, clusterId, + }, { enabled: isItemPublic !== undefined }); + + const dataOptions = useMemo(() => { + return items?.items.map(mapItemToOption) || []; + }, [items]); + + return { dataOptions, isDataLoading: isDataLoading && isItemPublic !== undefined }; +} + +export function useDataVersionOptions( + form: FormInstance, + dataType: DataType, + queryHook: QueryHookFunction, + mapItemToOption: (item: T) => Option, +): { dataVersionOptions: Option[], isDataVersionsLoading: boolean } { + const typePath = [dataType, "type"]; + const namePath = [dataType, "name"]; + const selectedItem = Form.useWatch(namePath, form); + const itemType = Form.useWatch(typePath, form); + const isItemPublic = itemType !== undefined ? itemType === AccessiblityType.PUBLIC : itemType; + + const { data: versions, isLoading: isDataVersionsLoading } = queryHook({ + [`${dataType}Id`]: selectedItem, isPublic : isItemPublic, + }, { enabled: selectedItem !== undefined }); + + const dataVersionOptions = useMemo(() => { + return versions?.items.map(mapItemToOption); + }, [versions]); + + return { dataVersionOptions, isDataVersionsLoading: isDataVersionsLoading && selectedItem !== undefined }; +} diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/runningJobs/page.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/runningJobs/page.tsx new file mode 100644 index 0000000000..4cd9ba77b6 --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/runningJobs/page.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { usePublicConfig } from "src/app/(auth)/context"; +import { PageTitle } from "src/components/PageTitle"; +import { NotFoundPage } from "src/layouts/error/NotFoundPage"; + +import { AppSessionsTable, AppTableStatus } from "../AppSessionsTable"; + +export default function Page({ params }: {params: {clusterId: string}}) { + const { clusterId } = params; + + const { publicConfig } = usePublicConfig(); + const cluster = publicConfig.CLUSTERS.find((x) => x.id === clusterId); + + + if (!cluster) { + return ; + } + + + return ( + <> + + + + ); +} diff --git a/apps/ai/src/app/(auth)/jobs/[clusterId]/trainJobs/page.tsx b/apps/ai/src/app/(auth)/jobs/[clusterId]/trainJobs/page.tsx new file mode 100644 index 0000000000..178f5921a9 --- /dev/null +++ b/apps/ai/src/app/(auth)/jobs/[clusterId]/trainJobs/page.tsx @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { LoadingOutlined } from "@ant-design/icons"; +import { PageTitle } from "src/components/PageTitle"; +import { ServerErrorPage } from "src/layouts/error/ServerErrorPage"; +import { trpc } from "src/utils/trpc"; + +import { LaunchAppForm } from "../LaunchAppForm"; + +export default function Page({ params }: {params: {clusterId: string}}) { + + const { clusterId } = params; + + const { data: clusterInfo, isLoading: isClusterLoading, isError } = + trpc.config.getClusterConfig.useQuery({ clusterId }); + + + if (isClusterLoading || !clusterInfo) { + return ; + } + + if (isError) { + return ( + + ); + } + + return ( +
+ + +
+ ); +} + + diff --git a/apps/ai/src/app/(auth)/layout.tsx b/apps/ai/src/app/(auth)/layout.tsx new file mode 100644 index 0000000000..d0b6c2c7a4 --- /dev/null +++ b/apps/ai/src/app/(auth)/layout.tsx @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { DatabaseOutlined, DesktopOutlined } from "@ant-design/icons"; +import React from "react"; +import { useUserQuery } from "src/app/auth"; +import { Loading } from "src/components/Loading"; +import { BaseLayout } from "src/layouts/base/BaseLayout"; +import { JumpToAnotherLink } from "src/layouts/base/header/Components"; +import { ServerErrorPage } from "src/layouts/error/ServerErrorPage"; +import { trpc } from "src/utils/trpc"; + +import { useUiConfig } from "../uiContext"; +import { PublicConfigContext } from "./context"; +import { defaultClusterContext } from "./defaultClusterContext"; +import { userRoutes } from "./routes"; + + +const useConfigQuery = () => { + return trpc.config.publicConfig.useQuery(); +}; + +export default function Layout( + { children }: + { children: React.ReactNode }, +) { + + const userQuery = useUserQuery(); + const configQuery = useConfigQuery(); + + if (userQuery.isLoading) { + return ( + + + + ); + } + + if (userQuery.isError || !userQuery.data.user) { + return; + } + + if (configQuery.isLoading) { + return ( + + {children} + + ); + } + + if (configQuery.isError) { + return ( + + + + ); + } + + const publicConfig = configQuery.data; + const { setDefaultCluster, defaultCluster } = defaultClusterContext(publicConfig.CLUSTERS); + + const { hostname, uiConfig } = useUiConfig(); + const footerConfig = uiConfig.config.footer; + const footerText = (hostname && footerConfig?.hostnameMap?.[hostname]) + ?? footerConfig?.defaultText ?? ""; + + const routes = userRoutes(userQuery.data.user, publicConfig, setDefaultCluster, defaultCluster); + + return ( + + } + link={publicConfig.MIS_URL} + // linkText={t("baseLayout.linkTextMis")} + linkText="管理系统" + /> + } + link={publicConfig.PORTAL_URL} + // linkText={t("baseLayout.linkTextAI")} + linkText="门户" + /> + {/* { + systemLanguageConfig.isUsingI18n ? ( + + ) : undefined + } */} + + )} + versionTag={publicConfig.VERSION_TAG} + footerText={footerText} + > + + {children} + + + ); + +} diff --git a/apps/ai/src/app/(auth)/model/CopyPublicModelModal.tsx b/apps/ai/src/app/(auth)/model/CopyPublicModelModal.tsx new file mode 100644 index 0000000000..907adae9a0 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/CopyPublicModelModal.tsx @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { ModelVersionInterface } from "src/models/Model"; +import { Cluster } from "src/server/trpc/route/config"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +export interface Props { + open: boolean; + modelId: number; + modelVersionId: number; + cluster?: Cluster; + modelName?: string; + data: ModelVersionInterface; + onClose: () => void; +} + +interface FormFields { + targetModalName: string + versionName: string, + versionDescription?: string, + path: string, +} + +export const CopyPublicModelModal: React.FC = ( + { open, onClose, modelId, modelVersionId, cluster, modelName, data }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const copyMutation = trpc.model.copyPublicModelVersion.useMutation({ + onSuccess() { + message.success("复制模型成功"); + onClose(); + form.resetFields(); + }, + onError(err) { + if (err.data?.code === "CONFLICT") { + form.setFields([ + { + name: "targetDatasetName", + errors: ["目标模型名称已存在"], + }, + ]); + return; + } + + message.error(err.message); + }, + }); + + + + + const onOk = async () => { + const { targetModalName, versionName, versionDescription, path } = await form.validateFields(); + copyMutation.mutate({ + modelId, + versionId: modelVersionId, + modelName: targetModalName, + versionName, + versionDescription: versionDescription ?? "", + path, + }); + }; + + return ( + +
+ + {modelName} + + + + + + {getI18nConfigCurrentText(cluster?.name, undefined)} + + + + + + + + + {data.algorithmVersion} + + + { + form.setFields([{ name: "path", value: path, touched: true }]); + form.validateFields(["path"]); + }} + clusterId={cluster?.id ?? ""} + /> + ) + } + /> + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/model/CreateAndEditModelModal.tsx b/apps/ai/src/app/(auth)/model/CreateAndEditModelModal.tsx new file mode 100644 index 0000000000..b7dcdb73dc --- /dev/null +++ b/apps/ai/src/app/(auth)/model/CreateAndEditModelModal.tsx @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal, Select } from "antd"; +import React from "react"; +import { SingleClusterSelector } from "src/components/ClusterSelector"; +import { AlgorithmTypeText, Framework } from "src/models/Algorithm"; +import { Cluster } from "src/server/trpc/route/config"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +interface EditProps { + cluster?: Cluster; + modelId: number; + modelName: string; + algorithmName?: string; + algorithmFramework?: Framework; + modalDescription?: string; +} +export interface Props { + open: boolean; + onClose: () => void; + refetch: () => void; + editData?: EditProps; +} + +interface FormFields { + modelName: string, + cluster: Cluster, + algorithmName: string, + algorithmFramework: Framework, + modalDescription: string, +} + +export const CreateAndEditModalModal: React.FC = ( + { open, onClose, refetch, editData }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const createModelMutation = trpc.model.createModel.useMutation({ + onSuccess() { + message.success("添加模型成功"); + form.resetFields(); + refetch(); + onClose(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("模型名称已存在"); + form.setFields([ + { + name: "modelName", + errors: ["模型名称已存在"], + }, + ]); + return; + } + message.error("添加模型失败"); + } }); + + const updateModelMutation = trpc.model.updateModel.useMutation({ + onSuccess() { + message.success("修改模型成功"); + refetch(); + onClose(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("模型名称已存在"); + form.setFields([ + { + name: "modelName", + errors: ["模型名称已存在"], + }, + ]); + } + else if (e.data?.code === "NOT_FOUND") { + message.error("模型未找到"); + } + else if (e.data?.code === "PRECONDITION_FAILED") { + message.error("有正在分享或正在取消分享的数据存在,请稍后再试"); + } + else { + message.error("修改模型失败"); + + } + } }); + + const onOk = async () => { + const { modelName:formModalName, cluster, algorithmName:formAlgorithmName, + algorithmFramework:formAlgorithmFramework, modalDescription:formModalDescription } = + await form.validateFields(); + + if (editData?.modelId) { + updateModelMutation.mutate({ + id:editData.modelId, + name:formModalName, + algorithmName:formAlgorithmName, + algorithmFramework:formAlgorithmFramework, + description:formModalDescription, + }); + } else { + createModelMutation.mutate({ + name:formModalName, + algorithmName:formAlgorithmName, + algorithmFramework:formAlgorithmFramework, + description:formModalDescription, + clusterId:cluster.id, + }); + } + }; + + return ( + +
+ + + + {editData?.cluster ? ( + + {getI18nConfigCurrentText(editData?.cluster?.name, undefined)} + + ) : ( + + + + )} + + + + + + + + + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/model/CreateAndEditVersionModal.tsx b/apps/ai/src/app/(auth)/model/CreateAndEditVersionModal.tsx new file mode 100644 index 0000000000..1856f7e510 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/CreateAndEditVersionModal.tsx @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { FileSelectModal } from "src/components/FileSelectModal"; +import { Cluster } from "src/server/trpc/route/config"; +import { validateNoChinese } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +interface EditProps { + versionId: number; + versionName: string; + versionDescription?: string; + algorithmVersion?: string; +} +export interface Props { + open: boolean; + onClose: () => void; + refetch: () => void; + modelId: number; + cluster?: Cluster; + modelName?: string; + editData?: EditProps; +} + +interface FormFields { + versionName: string, + versionDescription?: string, + algorithmVersion?: string, + path: string, +} + +export const CreateAndEditVersionModal: React.FC = ( + { open, onClose, modelId, cluster, modelName, refetch, editData }, +) => { + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const createModelVersionMutation = trpc.model.createModelVersion.useMutation({ + onSuccess() { + message.success("创建新版本成功"); + form.resetFields(); + onClose(); + refetch(); + form.resetFields(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("版本名称已存在"); + form.setFields([ + { + name: "versionName", + errors: ["版本名称已存在"], + }, + ]); + } else if (e.data?.code === "BAD_REQUEST") { + message.error("所选文件夹路径不存在"); + form.setFields([ + { + name: "path", + errors: ["所选文件夹路径不存在"], + }, + ]); + } else { + message.error("创建新版本失败"); + } + }, + }); + + + const updateModelVersionMutation = trpc.model.updateModelVersion.useMutation({ + onSuccess() { + message.success("修改版本成功"); + onClose(); + refetch(); + }, + onError(e) { + if (e.data?.code === "CONFLICT") { + message.error("版本名称已存在"); + form.setFields([ + { + name: "versionName", + errors: ["版本名称已存在"], + }, + ]); + } + else if (e.data?.code === "NOT_FOUND") { + message.error("模型或模型版本未找到"); + } + else if (e.data?.code === "PRECONDITION_FAILED") { + message.error("有正在分享或正在取消分享的数据存在,请稍后再试"); + } + else { + message.error(e.message); + } + }, + }); + + const onOk = async () => { + form.validateFields(); + const { versionName, versionDescription, algorithmVersion, path } = await form.validateFields(); + if (editData?.versionName && editData.versionId) { + updateModelVersionMutation.mutate({ + versionId: editData.versionId, + versionName, + versionDescription, + algorithmVersion, + modelId, + }); + } + else { + createModelVersionMutation.mutate({ + versionName, + versionDescription, + algorithmVersion, + path, + modelId, + }); + } + }; + + return ( + +
+ + {modelName} + + + {getI18nConfigCurrentText(cluster?.name, undefined)} + + + + + + + + + + + { + !editData?.versionId ? ( + + { + form.setFields([{ name: "path", value: path, touched: true }]); + form.validateFields(["path"]); + }} + clusterId={cluster?.id ?? ""} + /> + ) + } + /> + + ) : undefined + } + + +
+ ); +}; diff --git a/apps/ai/src/app/(auth)/model/ModelTable.tsx b/apps/ai/src/app/(auth)/model/ModelTable.tsx new file mode 100644 index 0000000000..2606f733f9 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/ModelTable.tsx @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { PlusOutlined } from "@ant-design/icons"; +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { App, Button, Form, Input, Modal, Space, Table, TableColumnsType } from "antd"; +import { useCallback, useState } from "react"; +import { SingleClusterSelector } from "src/components/ClusterSelector"; +import { FilterFormContainer } from "src/components/FilterFormContainer"; +import { ModalButton } from "src/components/ModalLink"; +import { ModelInterface } from "src/models/Model"; +import { Cluster } from "src/server/trpc/route/config"; +import { formatDateTime } from "src/utils/datetime"; +import { trpc } from "src/utils/trpc"; + +import { CreateAndEditModalModal } from "./CreateAndEditModelModal"; +import { CreateAndEditVersionModal } from "./CreateAndEditVersionModal"; +import { ModelVersionList } from "./ModelVersionList"; + +interface Props { + isPublic: boolean; + clusters: Cluster[]; +} + +interface FilterForm { + nameOrDesc?: string, + clusterId?: string, +} + +interface PageInfo { + page: number; + pageSize?: number; +} + +const CreateModalModalButton = +ModalButton(CreateAndEditModalModal, { type: "primary", icon: }); +const EditModalModalButton = +ModalButton(CreateAndEditModalModal, { type: "link" }); +const CreateVersionModalButton = ModalButton(CreateAndEditVersionModal, { type: "link" }); + +export const ModalTable: React.FC = ({ isPublic, clusters }) => { + const [{ confirm }, confirmModalHolder] = Modal.useModal(); + const { message } = App.useApp(); + + const [query, setQuery] = useState(() => { + return { + nameOrDesc: undefined, + framework: undefined, + clusterId:undefined, + }; + }); + + const [form] = Form.useForm(); + const [pageInfo, setPageInfo] = useState({ page: 1, pageSize: 10 }); + + const { data, isFetching, refetch, error } = trpc.model.list.useQuery( + { ...pageInfo, + nameOrDesc:query.nameOrDesc, + clusterId:query.clusterId, + isPublic, + }); + if (error) { + message.error("找不到模型"); + } + + const deleteModelMutation = trpc.model.deleteModel.useMutation({ + onSuccess() { + message.success("删除算法成功"); + refetch(); + }, + onError() { + message.error("删除模型失败"); + }, + }); + + const deleteModel = useCallback( + async (id: number) => { + confirm({ + title: "删除模型", + onOk:async () => { + await deleteModelMutation.mutateAsync({ id }); + }, + }); + }, + [], + ); + + const getCurrentCluster = useCallback((clusterId: string) => { + return clusters.find((c) => c.id === clusterId); + }, [clusters]); + + const columns: TableColumnsType = [ + { dataIndex: "name", title: "名称" }, + { dataIndex: "clusterId", title: "集群", + render: (_, r) => + getI18nConfigCurrentText(getCurrentCluster(r.clusterId)?.name, undefined) ?? r.clusterId }, + { dataIndex: "description", title: "模型描述" }, + { dataIndex: "algorithmName", title: "算法名称" }, + { dataIndex: "algorithmFramework", title: "算法框架" }, + { dataIndex: "versions", title: "版本数量", render:(versions) => versions.length }, + isPublic ? { dataIndex: "owner", title: "分享者" } : {}, + { dataIndex: "createTime", title: "创建时间", + render:(createTime) => formatDateTime(createTime), + }, + ...!isPublic ? [{ dataIndex: "action", title: "操作", + render: (_: any, r: ModelInterface) => { + return ( + <> + { refetch(); } } + modelId={r.id} + modelName={r.name} + cluster={getCurrentCluster(r.clusterId)} + > + 创建新版本 + + + 编辑 + + + + ); + }, + }] : [], + ]; + + return ( +
+ + + layout="inline" + form={form} + initialValues={query} + onFinish={async () => { + const { nameOrDesc } = await form.validateFields(); + setQuery({ ...query, nameOrDesc: nameOrDesc?.trim() }); + setPageInfo({ page: 1, pageSize: pageInfo.pageSize }); + }} + > + + { + setQuery({ ...query, clusterId:val.id }); + }} + /> + + + + + + + + + {!isPublic && ( + + 添加 + + )} + +
Object.keys(x).length)} + pagination={{ + current: pageInfo.page, + defaultPageSize: 10, + pageSize: pageInfo.pageSize, + showSizeChanger: true, + total: data?.count, + onChange: (page, pageSize) => setPageInfo({ page, pageSize }), + }} + expandable={{ + expandedRowRender: (record) => { + const cluster = getCurrentCluster(record.clusterId); + return cluster && ( + + ); + }, + }} + scroll={{ x: true }} + /> + + + {/* antd中modal组件 */} + {confirmModalHolder} + + ); +}; + diff --git a/apps/ai/src/app/(auth)/model/ModelVersionList.tsx b/apps/ai/src/app/(auth)/model/ModelVersionList.tsx new file mode 100644 index 0000000000..dc3127b3f6 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/ModelVersionList.tsx @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { TRPCClientError } from "@trpc/client"; +import { App, Button, Modal, Table } from "antd"; +import { useRouter } from "next/navigation"; +import React, { useCallback } from "react"; +import { ModalButton } from "src/components/ModalLink"; +import { SharedStatus } from "src/models/common"; +import { ModelInterface } from "src/models/Model"; +import { Cluster } from "src/server/trpc/route/config"; +import { AppRouter } from "src/server/trpc/router"; +import { getSharedStatusText } from "src/utils/common"; +import { formatDateTime } from "src/utils/datetime"; +import { trpc } from "src/utils/trpc"; + +import { CopyPublicModelModal } from "./CopyPublicModelModal"; +import { CreateAndEditVersionModal } from "./CreateAndEditVersionModal"; + +export interface Props { + isPublic?: boolean; + models: ModelInterface[]; + modelId: number; + modelName: string; + cluster: Cluster; +} + +const EditVersionModalButton = ModalButton(CreateAndEditVersionModal, { type: "link" }); +const CopyPublicModelModalButton = ModalButton(CopyPublicModelModal, { type: "link" }); + +export const ModelVersionList: React.FC = ( + { isPublic, modelId, modelName, cluster }, +) => { + const { message } = App.useApp(); + const [{ confirm }, confirmModalHolder] = Modal.useModal(); + const router = useRouter(); + + const { data: versionData, isFetching, refetch, error: versionError } = + trpc.model.versionList.useQuery({ modelId, isPublic }); + if (versionError) { + message.error("找不到模型版本"); + } + + const checkFileExist = trpc.file.checkFileExist.useMutation(); + + const shareMutation = trpc.model.shareModelVersion.useMutation({ + onSuccess() { + refetch(); + message.success("提交分享请求"); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "FORBIDDEN") { + message.error("没有权限分享此版本"); + return; + } + + message.error(err.message); + }, + }); + + const unShareMutation = trpc.model.unShareModelVersion.useMutation({ + onSuccess() { + refetch(); + message.success("提交取消分享请求"); + }, + onError: (err) => { + const { data } = err as TRPCClientError; + if (data?.code === "FORBIDDEN") { + message.error("没有权限取消分享此版本"); + return; + } + + message.error("取消分享失败"); + }, + }); + + const deleteModelVersionMutation = trpc.model.deleteModelVersion.useMutation({ + onSuccess() { + message.success("删除算法版本成功"); + refetch(); + }, + onError() { + message.error("删除模型版本失败"); + } }); + + const deleteModelVersion = useCallback( + (versionId: number, isConfirmed?: boolean) => { + confirm({ + title: isConfirmed ? "源文件已被删除,是否删除本条数据" : "删除模型版本", + onOk:async () => { + await deleteModelVersionMutation.mutateAsync({ versionId, modelId }); + }, + }); + }, + [modelId], + ); + + return ( + <> +
formatDateTime(createTime) }, + { dataIndex: "action", title: "操作", + ...isPublic ? {} : { width: 350 }, + render: (_, r) => { + return isPublic ? ( + + 复制 + + ) : + ( + <> + + 编辑 + + + + + + + ); + + }, + }, + ]} + /> + {/* antd中modal组件 */} + {confirmModalHolder} + + + ); +}; diff --git a/apps/ai/src/app/(auth)/model/page.tsx b/apps/ai/src/app/(auth)/model/page.tsx new file mode 100644 index 0000000000..40a7288791 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/page.tsx @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/algorithm/private"); +} diff --git a/apps/ai/src/app/(auth)/model/private/page.tsx b/apps/ai/src/app/(auth)/model/private/page.tsx new file mode 100644 index 0000000000..239869e556 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/private/page.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { ModalTable } from "src/app/(auth)/model/ModelTable"; +import { PageTitle } from "src/components/PageTitle"; + +export default function Page() { + + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/model/public/page.tsx b/apps/ai/src/app/(auth)/model/public/page.tsx new file mode 100644 index 0000000000..0d63e031d7 --- /dev/null +++ b/apps/ai/src/app/(auth)/model/public/page.tsx @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { ModalTable } from "src/app/(auth)/model/ModelTable"; +import { PageTitle } from "src/components/PageTitle"; + +export default function Page() { + const { publicConfig } = usePublicConfig(); + + return ( +
+ + +
+ ); +} diff --git a/apps/ai/src/app/(auth)/profile/ChangePasswordModal.tsx b/apps/ai/src/app/(auth)/profile/ChangePasswordModal.tsx new file mode 100644 index 0000000000..cdef087516 --- /dev/null +++ b/apps/ai/src/app/(auth)/profile/ChangePasswordModal.tsx @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { App, Form, Input, Modal } from "antd"; +import React from "react"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { confirmPasswordFormItemProps } from "src/utils/form"; +import { trpc } from "src/utils/trpc"; + +export interface Props { + open: boolean; + onClose: () => void; + identityId: string; +} + +interface FormInfo { + oldPassword: string; + newPassword: string; +} + +export const ChangePasswordModal: React.FC = ({ + open, + onClose, + identityId, +}) => { + + const { publicConfig } = usePublicConfig(); + const [form] = Form.useForm(); + const { message } = App.useApp(); + + const changePasswordMutation = trpc.auth.changePassword.useMutation({ + onSuccess() { + message.success("修改密码成功"); + form.resetFields(); + onClose(); + }, + onError(e) { + if (e.data?.code === "BAD_REQUEST") { + message.error(`修改密码失败:${e.message}`); + } + else if (e.data?.code === "CONFLICT") { + message.error("原密码错误"); + } + else { + message.error("修改密码失败"); + } + }, + }); + + const onFinish = async () => { + const { oldPassword, newPassword } = await form.validateFields(); + changePasswordMutation.mutate({ identityId, oldPassword, newPassword }); + }; + + return ( + +
+ + + + + + + + + + +
+ ); +}; + + diff --git a/apps/ai/src/app/(auth)/profile/page.tsx b/apps/ai/src/app/(auth)/profile/page.tsx new file mode 100644 index 0000000000..619abae809 --- /dev/null +++ b/apps/ai/src/app/(auth)/profile/page.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { Descriptions, Typography } from "antd"; +import { usePublicConfig } from "src/app/(auth)/context"; +import { ModalButton } from "src/components/ModalLink"; +import { Section } from "src/components/Section"; +import { antdBreakpoints } from "src/styles/constants"; +import { Head } from "src/utils/head"; +import { styled } from "styled-components"; + +import { ChangePasswordModal } from "./ChangePasswordModal"; + +const Container = styled.div` + display: flex; + flex-wrap: wrap; + flex-direction: column; +`; + +const Part = styled(Section)` + min-width: 400px; + max-width: 600px; + flex: 1; + margin: 0 8px 16px 0; + @media (min-width: ${antdBreakpoints.md}px) { + margin: 0 16px 32px 0; + } +`; + +const TitleText = styled(Typography.Title)` +&& { + width: 100vw; + font-weight: 700; + font-size: 24px; + padding: 0 0 10px 20px; + margin-left: -25px; + border-bottom: 1px solid #ccc; + @media (min-width: ${antdBreakpoints.md}px) { + padding: 0 0 20px 30px; + } +} +`; + +const ChangePasswordModalButton = ModalButton(ChangePasswordModal, { type: "link" }); + +export default function Page() { + + const { publicConfig, user } = usePublicConfig(); + + return ( + + + + 用户信息 + + + + + {user.identityId} + + + {user.name} + + + + { + publicConfig.ENABLE_CHANGE_PASSWORD ? ( + <> + + 修改密码 + + + + + ******** + + 修改密码 + + + + + + ) : undefined + } + + ); +} diff --git a/apps/ai/src/app/(auth)/routes.tsx b/apps/ai/src/app/(auth)/routes.tsx new file mode 100644 index 0000000000..e056e3da5e --- /dev/null +++ b/apps/ai/src/app/(auth)/routes.tsx @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { BookOutlined, DashboardOutlined, + DatabaseOutlined, FileImageOutlined, FolderOutlined, LinkOutlined, LockOutlined, OneToOneOutlined, + PlusOutlined, ShareAltOutlined, UngroupOutlined } from "@ant-design/icons"; +import { NavIcon } from "@scow/lib-web/build/layouts/icon"; +import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; +import { join } from "path"; +import { NavItemProps } from "src/layouts/base/NavItemProps"; +import { ClientUserInfo } from "src/server/trpc/route/auth"; +import { Cluster, NavLink, PublicConfig } from "src/server/trpc/route/config"; + +export const userRoutes: ( + user: ClientUserInfo | undefined, + publicConfig: PublicConfig, + setDefaultCluster: (cluster: Cluster) => void, + defaultCluster: Cluster, +) => NavItemProps[] = (user, publicConfig, setDefaultCluster, defaultCluster) => { + + if (!user) { return []; } + + return [ + { + Icon: DashboardOutlined, + text: "仪表盘", + path: "/dashboard", + clickToPath: "/dashboard", + }, + { + Icon: DatabaseOutlined, + text: "数据", + path: "/dataset", + clickToPath: "/dataset/private", + children: [ + { + Icon: LockOutlined, + text: "我的数据集", + path: "/dataset/private", + }, + { + Icon: ShareAltOutlined, + text: "公共数据集", + path: "/dataset/public", + }, + ], + }, + { + Icon: FileImageOutlined, + text: "镜像", + path: "/image", + clickToPath: "/image/private", + children: [ + { + Icon: LockOutlined, + text: "我的镜像", + path: "/image/private", + }, + { + Icon: ShareAltOutlined, + text: "公共镜像", + path: "/image/public", + }, + ], + }, + { + Icon: BookOutlined, + text: "作业", + path: "/jobs", + clickToPath: `/jobs/${defaultCluster.id}/createApps`, + children: [ + ...publicConfig.CLUSTERS.map((cluster) => ({ + Icon: FolderOutlined, + text: getI18nConfigCurrentText(cluster.name, undefined), + path: `/jobs/${cluster.id}`, + clickable: false, + children:[ + { + Icon: PlusOutlined, + text: "创建应用", + path: `/jobs/${cluster.id}/createApps`, + }, + { + Icon: PlusOutlined, + text: "训练", + path: `/jobs/${cluster.id}/trainJobs`, + }, + { + Icon: BookOutlined, + text: "正在运行的作业", + path: `/jobs/${cluster.id}/runningJobs`, + }, + { + Icon: BookOutlined, + text: "已完成的作业", + path: `/jobs/${cluster.id}/historyJobs`, + }, + ], + })), + ], + }, + { + Icon: UngroupOutlined, + text: "算法", + path: "/algorithm", + clickToPath: "/algorithm/private", + children: [ + { + Icon: LockOutlined, + text: "我的算法", + path: "/algorithm/private", + }, + { + Icon: ShareAltOutlined, + text: "公共算法", + path: "/algorithm/public", + }, + ], + }, + { + Icon: OneToOneOutlined, + text: "模型", + path: "/model", + clickToPath: "/model/private", + children: [ + { + Icon: LockOutlined, + text: "我的模型", + path: "/model/private", + }, + { + Icon: ShareAltOutlined, + text: "公共模型", + path: "/model/public", + }, + ], + }, + ...(publicConfig.CLUSTERS.length > 0 ? [ + { + Icon: FolderOutlined, + text: "文件管理", + path: "/files", + clickToPath: `/files/${defaultCluster.id}/~`, + children: publicConfig.CLUSTERS.map((cluster) => ({ + Icon: FolderOutlined, + text: cluster.name, + path: `/files/${cluster.id}`, + clickToPath: `/files/${cluster.id}/~`, + handleClick: () => { setDefaultCluster(cluster); }, + } as NavItemProps)), + }, + ] : []), + ...(publicConfig.NAV_LINKS && publicConfig.NAV_LINKS.length > 0 + ? publicConfig.NAV_LINKS.map((link) => { + + const parentNavPath = link.url ? `${link.url}?token=${user.token}` + : link.children?.length && link.children?.length > 0 + ? `${link.children[0].url}?token=${user.token}` : ""; + + return { + Icon: !link.iconPath ? LinkOutlined : ( + + ), + text: link.text, + path: parentNavPath, + clickToPath: parentNavPath, + clickable: true, + openInNewPage: link.openInNewPage, + children: link.children?.length ? link.children?.map((childLink: Omit & { + url: string; + }) => ({ + Icon: !childLink.iconPath ? LinkOutlined : ( + + ), + text: childLink.text, + path: `${childLink.url}?token=${user.token}`, + clickToPath: `${childLink.url}?token=${user.token}`, + clickable: true, + openInNewPage: childLink.openInNewPage, + } as NavItemProps)) : [], + } as NavItemProps; + }) : []), + ]; + +}; diff --git a/apps/ai/src/app/(auth)/swagger/page.tsx b/apps/ai/src/app/(auth)/swagger/page.tsx new file mode 100644 index 0000000000..5a797f3027 --- /dev/null +++ b/apps/ai/src/app/(auth)/swagger/page.tsx @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import "swagger-ui-react/swagger-ui.css"; + +import dynamic from "next/dynamic"; +import { join } from "path"; +import { Head } from "src/utils/head"; + +import { usePublicConfig } from "../context"; +const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false }); + +export default function Page() { + + const { publicConfig: { BASE_PATH } } = usePublicConfig(); + return ( + <> + + + + ); +} diff --git a/apps/ai/src/app/auth.ts b/apps/ai/src/app/auth.ts new file mode 100644 index 0000000000..a818834b06 --- /dev/null +++ b/apps/ai/src/app/auth.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +// import { deleteUserToken } from "src/server/auth/cookie"; +import { trpc } from "src/utils/trpc"; + + +export const useUserQuery = () => { + + return trpc.auth.getUserInfo.useQuery(undefined, { + // user info is never refreshed in the client + staleTime: Infinity, + }); +}; + +export const useLogoutMutation = () => { + return trpc.auth.logout.useMutation(); +}; + +export const useOptionalUser = () => { + const context = useUserQuery().data?.user; + return context; +}; + +export const useUser = () => { + + const user = useOptionalUser(); + if (!user) { throw new Error("not logged in"); } + return user; +}; diff --git a/apps/ai/src/app/clientLayout.tsx b/apps/ai/src/app/clientLayout.tsx new file mode 100644 index 0000000000..be46d79506 --- /dev/null +++ b/apps/ai/src/app/clientLayout.tsx @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { GlobalStyle } from "@scow/lib-web/build/layouts/globalStyle"; +import { usePathname } from "next/navigation"; +import { ErrorBoundary } from "src/components/ErrorBoundary"; +import { Loading } from "src/components/Loading"; +import { TopProgressBar } from "src/components/TopProgressBar"; +import { AntdConfigProvider } from "src/layouts/AntdConfigProvider"; +import { DarkModeCookie, DarkModeProvider } from "src/layouts/darkMode"; +import { RootErrorContent } from "src/layouts/error/RootErrorContent"; +import { AntdStyleRegistry } from "src/layouts/styleRegistry/AntdRegistry.jsx"; +import StyledComponentsRegistry from "src/layouts/styleRegistry/StyledComponentsRegistry.jsx"; +import { UiConfig } from "src/server/trpc/route/config"; +import { trpc } from "src/utils/trpc"; + +import { UiConfigContext } from "./uiContext"; + +export function ClientLayout(props: { + children: React.ReactNode, + initialDark?: DarkModeCookie, +}) { + const pathname = usePathname(); + + const useConfigQuery = () => { + return trpc.config.getUiConfig.useQuery(); + }; + + const useConfig = useConfigQuery(); + + const uiConfig = useConfig.data || {} as UiConfig; + + const host = (typeof window === "undefined") ? "" : location.host; + const hostname = host?.includes(":") ? host?.split(":")[0] : host; + const primaryColor = uiConfig.config?.primaryColor; + const color = (hostname && primaryColor?.hostnameMap?.[hostname]) + ?? primaryColor?.defaultColor ?? uiConfig.defaultPrimaryColor; + + return ( + + + + { + useConfig.isLoading ? + + : ( + + + + + + + {props.children} + + + + + ) + } + + + + + ); +} diff --git a/apps/ai/src/app/layout.tsx b/apps/ai/src/app/layout.tsx new file mode 100644 index 0000000000..2c70498fd8 --- /dev/null +++ b/apps/ai/src/app/layout.tsx @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import "antd/dist/reset.css"; + +import { DarkModeCookie } from "@scow/lib-web/build/layouts/darkMode"; +import { cookies } from "next/headers"; +import { join } from "path"; +import React from "react"; +import { ClientLayout } from "src/app/clientLayout"; +import { ServerClientProvider } from "src/app/trpcClient.server"; +import { BASE_PATH } from "src/utils/processEnv"; + +export default function MyApp({ children }: { children: React.ReactNode }) { + + const cookie = cookies(); + + const darkModeCookie = cookie.get("scow-dark"); + + const dark = darkModeCookie ? JSON.parse(darkModeCookie.value) as DarkModeCookie : undefined; + + return ( + + + + + + + + + {children} + + + + ); + +} diff --git a/apps/ai/src/app/page.tsx b/apps/ai/src/app/page.tsx new file mode 100644 index 0000000000..f42214995f --- /dev/null +++ b/apps/ai/src/app/page.tsx @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { redirect } from "next/navigation"; + +export default function Page() { + redirect("/dashboard"); +} diff --git a/apps/ai/src/app/trpcClient.server.tsx b/apps/ai/src/app/trpcClient.server.tsx new file mode 100644 index 0000000000..812e92c33f --- /dev/null +++ b/apps/ai/src/app/trpcClient.server.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { unstable_noStore as noStore } from "next/cache"; +import { ClientProvider } from "src/app/trpcClient"; +import { BASE_PATH } from "src/utils/processEnv"; + +function getBaseUrl() { + if (typeof window !== "undefined") + // browser should use relative path + return ""; + if (process.env.VERCEL_URL) + // reference for vercel.com + return `https://${process.env.VERCEL_URL}`; + if (process.env.RENDER_INTERNAL_HOSTNAME) + // reference for render.com + return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; + // assume localhost + return `http://localhost:${process.env.PORT ?? 3000}`; +} + + + +export const ServerClientProvider = (props: { children: React.ReactNode }) => { + noStore(); + + return ( + + {props.children} + + ); +}; diff --git a/apps/ai/src/app/trpcClient.tsx b/apps/ai/src/app/trpcClient.tsx new file mode 100644 index 0000000000..31311a36e1 --- /dev/null +++ b/apps/ai/src/app/trpcClient.tsx @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { joinWithUrl } from "@scow/utils"; +import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { httpBatchLink, loggerLink, TRPCClientError } from "@trpc/client"; +import { message } from "antd"; +import { join } from "path"; +import { useState } from "react"; +import { AppRouter } from "src/server/trpc/router"; +import { trpc } from "src/utils/trpc"; +import superjson from "superjson"; + +const MAX_RETRIES = 3; + +export function ClientProvider(props: { baseUrl: string; basePath: string; children: React.ReactNode }) { + + const [queryClient] = useState(() => new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry(failureCount, error) { + const { data } = error as TRPCClientError; + if (data?.code && data?.code === "UNAUTHORIZED") { + window.location.href = join(props.basePath, "/api/auth"); + return false; + } + + if (failureCount >= MAX_RETRIES) { + return false; + } + + return true; + }, + }, + }, + queryCache: new QueryCache({ + onError: (error, query) => { + const { data } = error as TRPCClientError; + + if (data?.code && data?.code === "UNAUTHORIZED") { + window.location.href = join(props.basePath, "/api/auth"); + } else if (data?.code && query?.meta?.[data?.code]) { + const msg = query?.meta?.[data?.code] as string; + message.error(msg); + } else { + message.error("出了一些问题,请稍后再试!"); + } + }, + }), + mutationCache: new MutationCache({ + onError: (error, variables, context, mutation) => { + const { data, message: errMessage } = error as TRPCClientError; + const { onError } = mutation.options; + if (data?.code && data?.code === "UNAUTHORIZED") { + window.location.href = join(props.basePath, "/api/auth"); + } else if (data?.path?.startsWith("file") && data?.code === "PRECONDITION_FAILED" + && errMessage.startsWith("SSH_ERROR:")) { + message.error("无法以用户身份连接到登录节点。请确认您的家目录的权限为700、750或者755,或您是否有权限在此执行操作"); + } else if (data?.path?.startsWith("file") && data?.code === "BAD_REQUEST" + && errMessage.startsWith("SFTP_ERROR:")) { + message.error(errMessage || "SFTP操作失败,请确认您是否有操作的权限"); + } else if (!onError) { + message.error("出了一些问题,请稍后再试!"); + } + }, + }), + })); + + const [trpcClient] = useState(() => + trpc.createClient({ + links: [ + loggerLink({ + enabled: () => process.env.NODE_ENV === "development", + }), + httpBatchLink({ + url: typeof window === "undefined" ? joinWithUrl(props.baseUrl, props.basePath, "/api/trpc") + : join(props.basePath, "/api/trpc"), + }), + ], + transformer: superjson, + }), + ); + + return ( + + + {props.children} + + + ); +} diff --git a/apps/ai/src/app/uiContext.ts b/apps/ai/src/app/uiContext.ts new file mode 100644 index 0000000000..0e5f110303 --- /dev/null +++ b/apps/ai/src/app/uiContext.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import React, { useContext } from "react"; +import { UiConfig } from "src/server/trpc/route/config"; + +export const UiConfigContext = React.createContext<{ + hostname: string, + uiConfig: UiConfig, +}>(undefined!); + +export const useUiConfig = () => { + return useContext(UiConfigContext); +}; diff --git a/apps/ai/src/components/AccountSelector.tsx b/apps/ai/src/components/AccountSelector.tsx new file mode 100644 index 0000000000..545741fdba --- /dev/null +++ b/apps/ai/src/components/AccountSelector.tsx @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { ReloadOutlined } from "@ant-design/icons"; +import { Button, Input, Select, Tooltip } from "antd"; +import { useEffect } from "react"; +import { trpc } from "src/utils/trpc"; + +interface Props { + cluster?: string; + value?: string; + onChange?: (value: string) => void; +} + +export const AccountSelector: React.FC = ({ cluster, onChange, value }) => { + + const { data, isLoading, refetch } = trpc.account.listAccounts.useQuery({ clusterId: cluster }); + + useEffect(() => { + + if (data && data.accounts.length) { + if (!value || !data.accounts.includes(value)) { + onChange?.(data.accounts[0]); + } + } + }, [data, value]); + + return ( + + v.id)} + onChange={(values) => onChange?.(values.map((x) => ({ + id: x, + name: publicConfig.CLUSTERS.find((cluster) => cluster.id === x)?.name ?? x })))} + options={publicConfig.CLUSTERS.map((x) => ({ value: x.id, label: + getI18nConfigCurrentText(x.name, languageId) }))} + key={languageId} + /> + ); +}; + +interface SingleSelectionProps { + value?: Cluster; + onChange?: (cluster: Cluster) => void; + label?: string; + clusterIds?: string[]; + allowClear?: boolean; +} + +export const SingleClusterSelector: React.FC = ({ + value, + onChange, + label, + clusterIds, + allowClear, +}) => { + + // const t = useI18nTranslateToString(); + // const languageId = useI18n().currentLanguage.id; + const languageId = "zh_cn"; + const { publicConfig } = usePublicConfig(); + const { setDefaultCluster } = defaultClusterContext(publicConfig.CLUSTERS); + + return ( +
column.key ? !hiddenColumns.includes(column.key as ColumnKey) : true) + : columns + } + size="small" + /> + ); +}; diff --git a/apps/ai/src/components/FilterFormContainer.tsx b/apps/ai/src/components/FilterFormContainer.tsx new file mode 100644 index 0000000000..63e1d4b836 --- /dev/null +++ b/apps/ai/src/components/FilterFormContainer.tsx @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +export * from "@scow/lib-web/build/components/FilterFormContainer"; diff --git a/apps/ai/src/components/LanguageSwitcher.tsx b/apps/ai/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000000..aa50622eea --- /dev/null +++ b/apps/ai/src/components/LanguageSwitcher.tsx @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { Select } from "antd"; +import { useRouter } from "next/router"; +import { setCookie } from "nookies"; +import { useEffect, useState } from "react"; +import { languageInfo, useI18n } from "src/i18n"; +import { styled } from "styled-components"; + + +const Container = styled.div` + white-space: nowrap; +`; + +interface LanguageSwitcherProps { + initialLanguage: string; +} + +export const LanguageSwitcher: React.FC = ({ initialLanguage }) => { + + const [selectedLanguage, setSelectedLanguage] = useState(""); + + // const { setLanguageId } = useStore(LoginNodeStore); + + const i18n = useI18n(); + + const router = useRouter(); + + useEffect(() => { + const init = i18n.currentLanguage.id; + if (init) { + setSelectedLanguage(init); + } else { + const defaultLanguage = initialLanguage; + setSelectedLanguage(defaultLanguage); + setLanguageCookie(defaultLanguage); + } + }, [router]); + + const setLanguage = (newLocale: string) => { + setSelectedLanguage(newLocale); + setLanguageCookie(newLocale); + i18n.setLanguageById(newLocale); + // setLanguageId(newLocale); + }; + + const setLanguageCookie = (newLocale: string) => { + setCookie(null, "language", newLocale, { + maxAge: 30 * 24 * 60 * 60, + path: "/", + }); + router.replace(router.asPath); + }; + + return ( + + + + ); +}; diff --git a/apps/ai/src/components/Loading.tsx b/apps/ai/src/components/Loading.tsx new file mode 100644 index 0000000000..044023c6b7 --- /dev/null +++ b/apps/ai/src/components/Loading.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { LoadingOutlined } from "@ant-design/icons"; +import { styled } from "styled-components"; + +const CenterLoading = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +`; + +export const Loading: React.FC = () => { + return ( + + + + + ); +}; diff --git a/apps/ai/src/components/MkdirModal.tsx b/apps/ai/src/components/MkdirModal.tsx new file mode 100644 index 0000000000..6adb70b17b --- /dev/null +++ b/apps/ai/src/components/MkdirModal.tsx @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { App, Form, Input, Modal } from "antd"; +import { join } from "path"; +import { trpc } from "src/utils/trpc"; + +interface Props { + open: boolean; + onClose: () => void; + reload: (() => void) | ((dirName: string) => Promise); + clusterId: string; + path: string; +} + +interface FormProps { + newDirName: string; +} + +export const MkdirModal: React.FC = ({ open, onClose, path, reload, clusterId }) => { + const { message } = App.useApp(); + const [form] = Form.useForm(); + + const mutation = trpc.file.mkdir.useMutation({ + onSuccess: () => { + message.success("创建成功"); + reload(form.getFieldValue("newDirName")); + onClose(); + form.resetFields(); + }, + onError: (e) => { + if (e.data?.code === "CONFLICT") { + message.error("已存在同名目录"); + } + }, + }); + + const onSubmit = async () => { + const { newDirName } = await form.validateFields(); + + mutation.mutate({ + path: join(path, newDirName), clusterId, + }); + }; + + return ( + +
+ + {path} + + label="目录名" name="newDirName" rules={[{ required: true }]}> + + + +
+ ); +}; diff --git a/apps/ai/src/components/ModalLink.tsx b/apps/ai/src/components/ModalLink.tsx new file mode 100644 index 0000000000..b4b6ca0be3 --- /dev/null +++ b/apps/ai/src/components/ModalLink.tsx @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Button, ButtonProps } from "antd"; +import React, { useState } from "react"; + +export interface CommonModalProps { + open: boolean; + onClose: () => void; +} + +export const ModalLink = ( + ModalComponent: React.ComponentType, +) => (props: React.PropsWithChildren>) => { + const [open, setOpen] = useState(false); + const { children, ...rest } = props; + + return ( + <> + setOpen(true)}> + {children} + + {/** @ts-ignore */} + { + setOpen(false); + }} + {...rest} + /> + + ); + + }; + + +export const ModalButton = ( + ModalComponent: React.ComponentType, + buttonProps?: ButtonProps, +) => (props: React.PropsWithChildren>) => { + const [open, setOpen] = useState(false); + const { children, ...rest } = props; + + return ( + <> + + {/** @ts-ignore */} + { + setOpen(false); + }} + {...rest} + /> + + ); + + }; diff --git a/apps/ai/src/components/PageTitle.tsx b/apps/ai/src/components/PageTitle.tsx new file mode 100644 index 0000000000..a0237afbf4 --- /dev/null +++ b/apps/ai/src/components/PageTitle.tsx @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Typography } from "antd"; +import React from "react"; +import { styled } from "styled-components"; + +const Container = styled.div` + margin: 0 0 8px 0; + display: flex; + align-items: center; + justify-content: space-between; +`; + +type PageTitleProps = React.PropsWithChildren<{ + beforeTitle?: React.ReactNode; + titleText: React.ReactNode; + isLoading?: boolean; + reload?: () => void; +}>; + +export const TitleText = styled(Typography.Title)` + && { + font-size: 28px; + } +`; + +export const PageTitle: React.FC = ({ + beforeTitle, titleText, children, +}) => { + return ( + + + {beforeTitle} + {titleText} + + {children} + + ); + +}; diff --git a/apps/ai/src/components/PathBar.tsx b/apps/ai/src/components/PathBar.tsx new file mode 100644 index 0000000000..835371c70f --- /dev/null +++ b/apps/ai/src/components/PathBar.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { ReloadOutlined, RightOutlined } from "@ant-design/icons"; +import { Breadcrumb, Button, Input } from "antd"; +import { useEffect, useState } from "react"; +import { styled } from "styled-components"; + +interface Props { + path: string; + loading: boolean; + onPathChange: (path: string) => void; + breadcrumbItemRender: (pathSegment: string, index: number, path: string) => React.ReactNode; + prefix?: React.ReactNode +} + +const Bar = styled.div` + display: flex; + width: 100%; +`; + +const BarStateBar = styled(Bar)` + border: 1px solid ${({ theme }) => theme.token.colorBorder}; + border-radius: ${({ theme }) => theme.token.borderRadius}px; + padding: 0 8px; + margin: 0 4px; + + .ant-breadcrumb { + align-self: center; + } +`; + +export const PathBar: React.FC = ({ + path, + loading, + onPathChange, + breadcrumbItemRender, + prefix, +}) => { + + const [state, setState] = useState<"bar" | "input">("bar"); + + const [input, setInput] = useState(path); + + useEffect(() => { + setInput(path); + }, [path]); + + const pathSegments = path === "/" ? [""] : path.split("/"); + + const icon = path === input + ? + : ; + + return ( + { + setInput(path); + setState("bar"); + }} + > + {state === "input" + ? ( + { + setInput(e.target.value); + }} + onSearch={onPathChange} + enterButton={icon} + autoFocus + prefix={prefix} + /> + ) : ( + <> + setState("input")}> + + {pathSegments.map((segment, index) => ( + + {breadcrumbItemRender(segment, index, pathSegments.slice(1, index + 1).join("/"))} + + ))} + + + , + ]} + > +

+ 文件将会上传到:{path}。同名文件将会被覆盖。 +

+

+ 单个上传文件大小最大为:{publicConfig.CLIENT_MAX_BODY_SIZE}。 +

+ urlToUpload(clusterId, join(path, file.name), publicConfig.BASE_PATH)} + withCredentials + showUploadList={{ + removeIcon: (file) => { + return ( + + ); + }, + }} + onChange={({ file, fileList }) => { + console.log(fileList); + const updatedFileList = [...fileList.filter((f) => f.status)]; + setUploadFileList(updatedFileList); + + if (file.status === "done") { + message.success(`${file.name}上传成功`); + reload(); + } else if (file.status === "error") { + message.error(`${file.name}上传失败`); + } + }} + beforeUpload={(file) => { + const fileMaxSize = parseInt(publicConfig.CLIENT_MAX_BODY_SIZE.slice(0, -1)) * (1024 ** 3); + + if (file.size > fileMaxSize) { + message.error(`${file.name}上传失败,文件大小超过${publicConfig.CLIENT_MAX_BODY_SIZE}`); + return Upload.LIST_IGNORE; + } + + return new Promise(async (resolve, reject) => { + const checkExistRes = await checkFileExist.mutateAsync({ path:join(path, file.name), clusterId }); + + if (checkExistRes.exists) { + modal.confirm({ + title: "文件已存在", + content: `文件: ${file.name}已存在,是否覆盖?`, + okText: "确认", + onOk: async () => { + const fileType = await getFileType.mutateAsync({ path:join(path, file.name), clusterId }); + + if (fileType.type) { + await deleteFileMutation.mutateAsync({ + target: fileType.type === FileType.DIR ? "DIR" : "FILE", + clusterId: clusterId, + path: join(path, file.name), + }).then(() => resolve(file)); + } + + }, + onCancel: () => { reject(file); }, + }); + } else { + resolve(file); + } + + }); + }} + fileList={uploadFileList} + > +

+ +

+

点击或者将文件拖动到这里

+

+ 支持上传单个或者多个文件 +

+
+ + ); +}; diff --git a/apps/ai/src/components/icons/README.md b/apps/ai/src/components/icons/README.md new file mode 100644 index 0000000000..31a843d88e --- /dev/null +++ b/apps/ai/src/components/icons/README.md @@ -0,0 +1,13 @@ +# SVG图标 + +从www.iconpacks.net处下载免费可商用的图标。 + +在代码中使用: + +```tsx +import Image from "next/image"; + +import moon from "src/components/icons/moon.svg"; + + +``` \ No newline at end of file diff --git a/apps/ai/src/components/icons/moon.svg b/apps/ai/src/components/icons/moon.svg new file mode 100644 index 0000000000..a1dd0e98a8 --- /dev/null +++ b/apps/ai/src/components/icons/moon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/ai/src/components/icons/sun-moon.svg b/apps/ai/src/components/icons/sun-moon.svg new file mode 100644 index 0000000000..4ea8e7424d --- /dev/null +++ b/apps/ai/src/components/icons/sun-moon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/ai/src/components/icons/sun.svg b/apps/ai/src/components/icons/sun.svg new file mode 100644 index 0000000000..ce3e641890 --- /dev/null +++ b/apps/ai/src/components/icons/sun.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ai/src/i18n/en.ts b/apps/ai/src/i18n/en.ts new file mode 100644 index 0000000000..074993f94f --- /dev/null +++ b/apps/ai/src/i18n/en.ts @@ -0,0 +1,535 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export default { + // baseLayout + baseLayout: { + linkTextMis: "Management System", + linkTextPortal: "PORTAL", + }, + // routes + routes: { + dashboard: "Dashboard", + job: { + title: "Jobs", + runningJobs: "Running Jobs", + allJobs: "All Jobs", + submitJob: "Submit Job", + jobTemplates: "Job Templates", + }, + desktop: "Desktop", + apps: { + title: "Interactive Apps", + appSessions: "Created Apps", + createApp: "Create App", + }, + file: { + fileManager: "File Manager", + crossClusterFileTransfer: "File Transfer", + clusterFileManager: "Cluster File Manager", + transferProgress: "Transfer Progress", + }, + }, + // button + button: { + searchButton: "Search", + refreshButton: "Refresh", + cancelButton: "Cancel", + confirmButton: "Confirm", + selectButton: "Select", + actionButton: "Action", + deleteButton: "Delete", + renameButton: "Rename", + finishButton: "Finish", + detailButton: "Details", + submitButton: "Submit", + closeButton: "Close", + startButton: "Start", + }, + // pageComp + pageComp: { + // profile + profile: { + changPasswordModal: { + successMessage: "Password changed successfully", + errorMessage: "Incorrect original password", + changePassword: "Change Password", + oldPassword: "Old Password", + newPassword: "New Password", + confirm: "Confirm Password", + }, + }, + // job + job: { + accountSelector: { + selectAccountPlaceholder: "Select Account", + refreshAccountList: "Refresh Account List", + }, + allJobsTable: { + searchForm: { + clusterLabel: "Cluster", + time: "Time", + popoverTitle: "Query all active jobs (such as job submission, pending, started, running, " + + "failed, completed) in this time range", + jobId: "Job ID", + }, + tableInfo: { + jobId: "Job ID", + jobName: "Job Name", + account: "Account", + partition: "Partition", + qos: "QOS", + state: "State", + submitTime: "Submission Time", + startTime: "Start Time", + endTime: "End Time", + elapsed: "Elapsed Time", + timeLimit: "Job Time Limit", + reason: "Reason", + more: "More", + linkToPath: "Go to Directory", + }, + }, + // fileSelectModal + fileSelectModal: { + title: "File Directory Selection Box", + newPath: "New Directory", + }, + // jobTemplateModal + jobTemplateModal: { + clusterLabel: "Cluster", + errorMessage: "Template does not exist!", + changeSuccessMessage: "Modified successfully!", + changTemplateName: "Change Template Name", + newTemplateName: "New Template Name", + templateName: "Template Name", + comment: "Comment", + useTemplate: "Use Template", + popConfirm: "Are you sure you want to delete this template?", + deleteSuccessMessage: "Template has been deleted!", + }, + // runningJobDrawer + runningJobDrawer: { + cluster: "Cluster", + jobId: "Job ID", + account: "Account", + jobName: "Job Name", + partition: "Partition", + qos: "QOS", + nodes: "Number of Nodes", + cores: "Number of Cores", + gpus: "Number of GPU Cards", + state: "State", + nodesOrReason: "Reason", + runningOrQueueTime: "Running/Queue Time", + submissionTime: "Submission Time", + timeLimit: "Job Time Limit (minutes)", + drawerTitle: "Details of Running Jobs", + }, + // runningJobTable + runningJobTable: { + filterForm: { + cluster: "Cluster", + jobId: "Job ID", + }, + jobInfoTable: { + cluster: "Cluster", + jobId: "Job ID", + account: "Account", + name: "Job Name", + partition: "Partition", + qos: "QOS", + nodes: "Nodes", + cores: "Cores", + gpus: "GPUs", + state: "State", + runningOrQueueTime: "Running/Queue Time", + nodesOrReason: "Reason", + timeLimit: "Job Time Limit", + more: "More", + linkToPath: "Go to Directory", + popConfirm: "Are you sure you want to terminate this task?", + successMessage: "Task termination request has been submitted!", + }, + }, + // submitJobForm + submitJobForm: { + errorMessage: "Failed to submit job", + successMessage: "Submitted successfully! Your new job ID is: ", + cluster: "Cluster", + jobName: "Job Name", + command: "Command", + account: "Account", + partition: "Partition", + qos: "QOS", + nodeCount: "Number of Nodes", + gpuCount: "Number of GPU Cards per Node", + coreCount: "Number of CPU Cores per Node", + maxTime: "Maximum Running Time", + minute: "Minutes", + workingDirectory: "Working Directory", + wdTooltip1: "1. Please enter the absolute path. If you enter a relative path, it will be " + + "relative to the user's home directory.", + wdTooltip2: "2. If the specified directory is not accessible or cannot be operated on, " + + "the job submission or execution will fail.", + output: "Standard Output File", + errorOutput: "Error Output File", + totalNodeCount: "Total Nodes: ", + totalGpuCount: "Total GPU Cards: ", + totalCoreCount: "Total CPU Cores: ", + totalMemory: "Total Memory Capacity: ", + comment: "Comment", + saveToTemplate: "Save as Template", + }, + }, + fileManagerComp: { + clusterFileTable: { + fileName: "File Name", + modificationDate: "Modification Date", + size: "Size", + permission: "Permission", + notShowHiddenItem: "Do not show hidden items", + showHiddenItem: "Show hidden items", + }, + singleCrossClusterTransferSelector: { + placeholder: "Please select a cluster", + }, + transferInfoTable: { + srcCluster: "Source Cluster", + dstCluster: "Destination Cluster", + file: "File", + transferCount: "Transfer Count", + transferSpeed: "Transfer Speed", + timeLeft: "Time Remaining", + currentProgress: "Current Progress", + operation: "Operation", + confirmCancelTitle: "Confirm Cancellation", + confirmCancelContent: "Are you sure you want to cancel the transfer of the file {} from {} to {}?", + confirmOk: "Confirm", + cancelSuccess: "Cancellation Successful", + cancel: "Cancel", + }, + fileEditModal: { + edit: "Edit", + prompt: "Prompt", + save: "Save", + doNotSave: "Do Not Save", + notSaved: "Not Saved", + notSavePrompt: "The file has not been saved, do you want to save this file?", + fileEdit: "File Edit", + filePreview: "File Preview", + fileLoading: "File is loading...", + exitEdit: "Exit Edit Mode", + failedGetFile: "Failed to get file: {}", + cantReadFile: "Cannot read file: {}", + saveFileFail: "File save failed: {}", + saveFileSuccess: "File saved successfully", + fileSizeExceeded: "File too large (maximum {}), please download and edit", + }, + createFileModal: { + createErrorMessage: "File or directory with the same name already exists!", + createSuccessMessage: "Created successfully", + create: "Create File", + fileDirectory: "Directory to Create File", + fileName: "File Name", + }, + fileManager: { + preview: { + cantPreview: "File too large (maximum {}) or format not supported, please download to view", + }, + moveCopy: { + copy: "Copy", + move: "Move", + modalErrorTitle: "Error {} {}", + existModalTitle: "File/Directory Already Exists", + existModalContent: "File/Directory {} already exists. Do you want to overwrite it?", + existModalOk: "Confirm", + errorMessage: "{} error! A total of {} files/directories, {} succeeded, {} abandoned, {} failed", + successMessage: "{} succeeded! A total of {} files/directories, {} succeeded, {} abandoned", + }, + delete: { + confirmTitle: "Confirm Deletion", + confirmOk: "Confirm", + confirmContent: "Are you sure you want to delete selected {}?", + successMessage: "Deleted {} successfully!", + errorMessage: "Deleted {} items, failed {} items", + otherErrorMessage: "Error occurred while performing delete operation", + }, + tableInfo: { + title: "Cluster {} File Management", + uploadButton: "Upload File", + deleteSelected: "Delete Selected", + copySelected: "Copy Selected", + moveSelected: "Move Selected", + paste: "Paste Here", + operationStarted: "{} in progress, completed: ", + operationNotStarted: "Selected {} {} items", + notShowHiddenItem: "Do not Show Hidden Items", + showHiddenItem: "Show Hidden Items", + openInShell: "Open in Terminal", + createFile: "New File", + mkDir: "New Directory", + download: "Download", + rename: "Rename", + deleteConfirmTitle: "Confirm Deletion", + deleteConfirmContent: "Confirm deletion of {}?", + deleteConfirmOk: "Confirm", + deleteSuccessMessage: "Deleted successfully", + submitConfirmTitle: "Submit Confirmation", + submitConfirmContent: "Confirm submission of {} to {}?", + submitConfirmOk: "Confirm", + submitSuccessMessage: "Submitted successfully! Your new job ID is: {}", + submitFailedMessage: "Submitted Failed", + }, + }, + fileTable: { + fileName: "File Name", + changeTime: "Modification Date", + size: "Size", + mode: "Permissions", + action: "Action", + }, + mkDirModal: { + existedErrorMessage: "File or directory with the same name already exists!", + successMessage: "Created successfully", + title: "Create Directory", + mkdirLabel: "Directory to Create", + dirName: "Directory Name", + }, + renameModal: { + successMessage: "Renamed successfully", + title: "Rename File", + renameLabel: "File to Rename", + newFileName: "New File Name", + }, + uploadModal: { + title: "Upload File", + uploadRemark1: "File will be uploaded to: ", + uploadRemark2: ". Files with the same name will be overwritten. ", + uploadRemark3: "Maximum file size for single upload: ", + uploadRemark4: ".", + cancelUpload: "Cancel Upload", + deleteUploadRecords: "Delete Upload Records", + successMessage: "Uploaded successfully", + errorMessage: "Upload failed", + maxSizeErrorMessage: "{} upload failed, file size exceeded {} ", + existedModalTitle: "File/Directory Already Exists", + existedModalContent: "File/Directory {} already exists. Do you want to overwrite it?", + existedModalOk: "Confirm", + dragText: "Click or drag files here", + hintText: "Supports uploading single or multiple files", + }, + }, + // desktop + desktop: { + desktopTable: { + tableItem: { + title: "Desktop ID", + desktopName: "Desktop Name", + wm: "Desktop Type", + addr: "Address", + createTime: "Creation Time", + }, + filterForm: { + cluster: "Cluster", + loginNode: "Login Node", + createNewDesktop: "Create New Desktop", + }, + }, + desktopTableActions: { + popConfirmTitle: "This action is irreversible. Are you sure you want to delete?", + }, + newDesktopModal: { + error: { + tooManyTitle: "Failed to Create Desktop", + tooManyContent: "The number of desktops in this cluster has reached its maximum limit.", + }, + modal: { + createNewDesktop: "Create New Desktop", + loginNode: "Login Node", + wm: "Desktop", + desktopName: "Desktop Name", + }, + }, + }, + // app + app: { + appSessionTable: { + table: { + sessionId: "Job Name", + jobId: "Job ID", + appId: "Application", + submitTime: "Submission Time", + state: "Status", + remainingTime: "Remaining Time", + popFinishConfirmTitle: "Are you sure you want to end this task?", + popFinishConfirmMessage: "Task termination request has been submitted.", + popCancelConfirmTitle: "Are you sure you want to cancel this task?", + popCancelConfirmMessage: "Task cancellation request has been submitted.", + linkToPath: "Enter Directory", + }, + filterForm: { + appJobName: "Job Name", + autoRefresh: "Auto-refresh every 10s", + onlyNotEnded: "Show only unended tasks", + }, + }, + connectToAppLink: { + notFoundMessage: "This application session does not exist.", + notConnectableMessage: "This application cannot be connected at the moment.", + notReady: "Application is not ready yet.", + connect: "Connect", + }, + createApps: { + notFoundMessage: "No interactive application available for creation.", + loading: "Loading available interactive applications...", + create: "Create", + }, + launchAppForm: { + errorMessage: "Failed to create application.", + successMessage: "Successfully created.", + loading: "Retrieving last submission records...", + appJobName: "Job Name", + account: "Account", + partition: "Partition", + qos: "QOS", + nodeCount: "Node Count", + gpuCount: "GPU Cards per Node", + coreCount: "CPU Cores per Node", + maxTime: "Maximum Running Time", + minute: "Minutes", + totalGpuCount: "Total GPU Cards", + totalCpuCount: "Total CPU Cores", + totalMemory: "Total Memory Capacity", + appCommentTitle: "Explanation", + }, + }, + }, + component: { + errorPages: { + notAllowedPage: "Access to this page is not allowed.", + systemNotAllowed: "The system does not allow access to this page.", + notAllowed: "Access Denied", + needLogin: "Login Required", + notLogin: "You are either not logged in or your login session has expired. " + + "You need to login to access this page.", + login: "Login", + notExist: "Does Not Exist", + pageNotExist: "The page you requested does not exist.", + serverWrong: "Server Error", + sorry: "Sorry, there was a server error. Please refresh and try again.", + }, + others: { + clusterSelector: "Please select a cluster.", + }, + }, + pages: { + apps: { + create: { + title: "Create ", + error404: "This app does not exist", + }, + createApps: { + subTitle: "The requested cluster does not exist", + title: "Create App", + pageTitle: "Create an App on {} Cluster", + }, + sessions: { + subTitle: "The requested cluster does not exist", + title: "Interactive Apps", + pageTitle: "{} Cluster Interactive Apps", + }, + }, + desktop: { + title: "Desktop", + pageTitle: "Desktop on Login Node", + }, + files: { + path: { + subTitle: "The requested cluster does not exist", + title: "File Management", + }, + fileTransfer: { + confirmTransferTitle: "Confirm to start transfer?", + confirmTransferContent: "Are you sure to transfer from {} to {}?", + confirmOk: "Confirm", + transferStartInfo: "Transfer task has started", + transferTitle: "Cross-cluster file transfer", + }, + currentTransferInfo: { + checkTransfer: "Check file transfer progress", + }, + }, + jobs: { + allJobs: { + title: "Historical Jobs", + pageTitle: "All Historical Jobs of this User", + }, + runningJobs: { + title: "Running Jobs", + pageTitle: "Unfinished Jobs of this User", + }, + savedJobs: { + title: "Job Templates", + pageTitle: "Job Template List", + }, + submit: { + title: "Submit Job", + pageTitle: "Submit Job", + spin: "Loading job templates", + }, + }, + profile: { + title: "Account Information", + userInfo: "User Information", + identityId: "User ID", + name: "User Name", + changePassword: "Change Password", + loginPassword: "Login Password", + }, + shell: { + loginNode: { + title: "Terminal", + content: "Connected to {} cluster's {} node with ID: {}", + reloadButton: "Refresh and Reconnect", + popoverTitle: "Commands", + popoverContent1: "Navigate to the file system ", + popoverContent2: "After entering this command, you will navigate to the file system, where you " + + "can upload and download files.", + popoverContent3: "Download a file", + popoverContentFile: "File Name", + popoverContent4: "By entering", + popoverContent5: ", the file in your current path will be downloaded locally. Relative paths " + + "are not supported at the moment.", + popoverContent6: "If you need to download files from other directories, please use", + popoverContent7: "command to navigate to the file system.", + popoverContent8: "Usage example: ", + command: "Command", + }, + index: { + title: "Terminal", + content: "Launch terminal for the following clusters: ", + }, + }, + _app: { + sshError: "Unable to connect as a user to the login node. Please make sure the permissions " + + "of your home directory are 700, 750, or 755.", + textExceedsLength:"There are too many welcome messages for terminal login." + + "Please reduce unnecessary information output!", + sftpError: "SFTP operation failed. Please confirm if you have the necessary permissions.", + otherError: "Server encountered an error!", + }, + dashboard: { + title: "Dashboard", + }, + }, +}; diff --git a/apps/ai/src/i18n/index.ts b/apps/ai/src/i18n/index.ts new file mode 100644 index 0000000000..8f856a8937 --- /dev/null +++ b/apps/ai/src/i18n/index.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { createI18n, Lang, languageDictionary, TextIdFromLangDict } from "react-typed-i18n"; + + +const zh_cn = () => import("./zh_cn").then((x) => x.default); +const en = () => import("./en").then((x) => x.default); + +// return language type +type LangType = Awaited>; + +export const languages = languageDictionary({ + zh_cn, + en, +}); + +export const languageInfo = { + zh_cn: { name: "CN 简体中文" }, + en: { name: "US English" }, +}; + +export const { Localized, Provider, id, prefix, useI18n } = createI18n(languages); + +export type TextId = TextIdFromLangDict; + +export function useI18nTranslate() { + const i18n = useI18n(); + + const tArgs = (id: Lang, args: React.ReactNode[] = []): string | React.ReactNode => { + return i18n.translate(id, args); + }; + + return tArgs; +} + +export function useI18nTranslateToString() { + const i18n = useI18n(); + + const t = (id: Lang, args: React.ReactNode[] = []): string => { + return i18n.translateToString(id, args); + }; + + return t; +} + +export type TransType = (id: Lang, args?: React.ReactNode[]) => string; diff --git a/apps/ai/src/i18n/zh_cn.ts b/apps/ai/src/i18n/zh_cn.ts new file mode 100644 index 0000000000..533ea42708 --- /dev/null +++ b/apps/ai/src/i18n/zh_cn.ts @@ -0,0 +1,533 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export default { + // baseLayout + baseLayout: { + linkTextMis: "管理系统", + linkTextPortal: "门户", + }, + // routes + routes: { + dashboard: "仪表盘", + job:{ + title: "作业", + runningJobs: "未结束的作业", + allJobs: "所有作业", + submitJob: "提交作业", + jobTemplates: "作业模板", + }, + desktop: "桌面", + apps: { + title: "交互式应用", + appSessions: "已创建的应用", + createApp: "创建应用", + }, + file: { + fileManager: "文件管理", + crossClusterFileTransfer: "文件传输", + clusterFileManager: "集群文件管理", + transferProgress: "传输进度", + }, + }, + // button + button: { + searchButton: "搜索", + refreshButton: "刷新", + cancelButton: "取消", + confirmButton: "确定", + selectButton: "选择", + actionButton: "操作", + deleteButton: "删除", + renameButton: "重命名", + finishButton: "结束", + detailButton: "详情", + submitButton: "提交", + closeButton: "关闭", + startButton: "启动", + }, + // pageComp + pageComp: { + // profile + profile: { + changPasswordModal: { + successMessage: "密码更改成功", + errorMessage: "原密码错误", + changePassword: "修改密码", + oldPassword:"原密码", + newPassword: "新密码", + confirm: "确认密码", + }, + }, + // job + job: { + accountSelector: { + selectAccountPlaceholder: "请选择账户", + refreshAccountList: "刷新账户列表", + }, + allJobsTable: { + searchForm: { + clusterLabel: "集群", + time: "时间", + popoverTitle: "查询该时间区域内所有有活动(如作业提交、等待中、开始、运行、失败、完成)的作业", + + jobId: "作业ID", + }, + tableInfo: { + jobId: "作业ID", + jobName: "作业名", + account: "账户", + partition: "分区", + qos: "QOS", + state: "状态", + submitTime: "提交时间", + startTime: "开始时间", + endTime: "结束时间", + elapsed: "运行时间", + timeLimit: "作业时间限制", + reason: "说明", + more: "更多", + linkToPath: "进入目录", + }, + }, + // fileSelectModal + fileSelectModal: { + title: "文件目录选择框", + newPath: "新目录", + }, + // jobTemplateModal + jobTemplateModal: { + clusterLabel:"集群", + errorMessage: "模板不存在!", + changeSuccessMessage: "修改成功!", + changTemplateName: "修改模板名字", + newTemplateName: "新模板名", + templateName: "模板名", + comment: "备注", + useTemplate:"使用模板", + popConfirm: "确定删除这个模板吗?", + deleteSuccessMessage: "模板已删除!", + }, + // runningJobDrawer + runningJobDrawer: { + cluster: "集群", + jobId: "作业ID", + account: "账户", + jobName: "作业名", + partition: "分区", + qos: "QOS", + nodes: "节点数(个)", + cores: "核心数(个)", + gpus: "GPU卡数(个)", + state: "状态", + nodesOrReason: "说明", + runningOrQueueTime: "运行/排队时间", + submissionTime: "提交时间", + timeLimit: "作业时限(分钟)", + drawerTitle: "未结束的作业详细信息", + }, + // runningJobTable + runningJobTable: { + filterForm: { + cluster: "集群", + jobId: "作业ID", + }, + jobInfoTable: { + cluster: "集群", + jobId: "作业ID", + account: "账户", + name: "作业名", + partition: "分区", + qos: "QOS", + nodes: "节点数", + cores: "核心数", + gpus: "GPU卡数", + state: "状态", + runningOrQueueTime: "运行/排队时间", + nodesOrReason: "说明", + timeLimit: "作业时间限制", + more: "更多", + linkToPath: "进入目录", + popConfirm: "确定结束这个任务吗?", + successMessage: "任务结束请求已经提交!", + }, + }, + // submitJobForm + submitJobForm: { + errorMessage: "提交作业失败", + successMessage: "提交成功!您的新作业ID为:", + cluster: "集群", + jobName: "作业名", + command: "命令", + account: "账户", + partition: "分区", + qos: "QOS", + nodeCount: "节点数", + gpuCount: "单节点GPU卡数", + coreCount: "单节点核心数", + maxTime: "最长运行时间", + minute: "分钟", + workingDirectory: "工作目录", + wdTooltip1: "1. 请填写绝对路径,如填写相对路径,则相对于该用户家目录;", + + wdTooltip2: "2. 填写目录不可访问或者不可操作时,提交作业或者作业运行将失败;", + + output: "标准输出文件", + errorOutput: "错误输出文件", + totalNodeCount: "总节点数:", + totalGpuCount: "总GPU卡数:", + totalCoreCount: "总CPU核心数:", + totalMemory: "总内存容量:", + comment: "备注", + saveToTemplate: "保存为模板", + }, + }, + fileManagerComp: { + clusterFileTable: { + fileName: "文件名", + modificationDate: "修改日期", + size: "大小", + permission: "权限", + notShowHiddenItem: "不显示隐藏的项目", + showHiddenItem: "显示隐藏的项目", + }, + singleCrossClusterTransferSelector: { + placeholder: "请选择集群", + }, + transferInfoTable: { + srcCluster: "发送集群", + dstCluster: "接收集群", + file: "文件", + transferCount: "传输数量", + transferSpeed: "传输速度", + timeLeft: "剩余时间", + currentProgress: "当前进度", + operation: "操作", + confirmCancelTitle: "确认取消", + confirmCancelContent: "确认取消 {} -> {} 的文件 {} 的传输吗?", + confirmOk: "确认", + cancelSuccess: "取消成功", + cancel: "取消", + }, + fileEditModal: { + edit: "编辑", + prompt: "提示", + save: "保存", + doNotSave: "不保存", + notSaved: "未保存", + notSavePrompt: "文件未保存,是否保存该文件?", + fileEdit: "文件编辑", + filePreview: "文件预览", + fileLoading: "文件正在加载...", + exitEdit: "退出编辑", + failedGetFile: "获取文件: {} 失败", + cantReadFile: "无法读取文件: {}", + saveFileFail: "文件保存失败: {}", + saveFileSuccess: "文件保存成功", + fileSizeExceeded: "文件过大(最大{}),请下载后编辑", + }, + createFileModal: { + createErrorMessage: "同名文件或者目录已经存在!", + createSuccessMessage: "创建成功", + create: "创建文件", + fileDirectory: "要创建的文件的目录", + fileName: "文件名", + }, + fileManager: { + preview: { + cantPreview: "文件过大(最大{})或者格式不支持,请下载后查看", + }, + moveCopy: { + copy: "复制", + move: "移动", + modalErrorTitle: "文件{}{}出错", + existModalTitle: "文件/目录已存在", + existModalContent: "文件/目录{}已存在,是否覆盖?", + existModalOk: "确认", + errorMessage: "{}错误!总计{}项文件/目录,其中成功{}项,放弃{}项,失败{}项", + successMessage: "{}成功!总计{}项文件/目录,其中成功{}项,放弃{}项", + }, + delete: { + confirmTitle: "确认删除", + confirmOk: "确认", + confirmContent: "确认要删除选中的{}项?", + successMessage: "删除{}项成功!", + errorMessage: "删除成功{}项,失败{}项", + otherErrorMessage: "执行删除操作时遇到错误", + }, + tableInfo: { + title: "集群{}文件管理", + uploadButton: "上传文件", + deleteSelected: "删除选中", + copySelected: "复制选中", + moveSelected: "移动选中", + paste: "粘贴到此处", + operationStarted: "正在{},已完成:", + operationNotStarted: "已选择{}{}个项", + notShowHiddenItem: "不显示隐藏的项目", + showHiddenItem: "显示隐藏的项目", + openInShell: "在终端中打开", + createFile: "新文件", + mkDir: "新目录", + download: "下载", + rename: "重命名", + deleteConfirmTitle: "确认删除", + deleteConfirmContent: "确认删除{}?", + deleteConfirmOk: "确认", + deleteSuccessMessage: "删除成功", + submitConfirmTitle: "确认提交", + submitConfirmContent: "确认提交{}至{}?", + submitConfirmOk: "确认", + submitSuccessMessage: "提交成功!您的新作业ID为:{}", + submitFailedMessage: "提交失败", + }, + }, + fileTable: { + fileName: "文件名", + changeTime: "修改日期", + size: "大小", + mode: "权限", + action: "操作", + }, + mkDirModal: { + existedErrorMessage: "同名文件或目录已经存在!", + successMessage: "创建成功", + title: "创建目录", + mkdirLabel: "要创建的目录的目录", + dirName: "目录名", + }, + renameModal: { + successMessage: "修改成功", + title: "重命名文件", + renameLabel: "要重命名的文件", + newFileName: "新文件名", + }, + uploadModal: { + title: "上传文件", + uploadRemark1: "文件将会上传到:", + uploadRemark2: "。同名文件将会被覆盖。", + uploadRemark3: "单个上传文件大小最大为:", + uploadRemark4: "。", + cancelUpload: "取消上传", + deleteUploadRecords: "删除上传记录", + successMessage: "上传成功", + errorMessage: "上传失败", + maxSizeErrorMessage: "{}上传失败,文件大小超过{}", + existedModalTitle: "文件/目录已存在", + existedModalContent: "文件/目录{}已存在,是否覆盖?", + existedModalOk: "确认", + dragText: "点击或者将文件拖动到这里", + hintText: "支持上传单个或者多个文件", + }, + }, + // desktop + desktop: { + desktopTable: { + tableItem: { + title: "桌面ID", + desktopName: "桌面名称", + wm: "桌面类型", + addr: "地址", + createTime: "创建时间", + }, + filterForm: { + cluster: "集群", + loginNode: "登录节点", + createNewDesktop: "新建桌面", + }, + }, + desktopTableActions: { + popConfirmTitle: "删除后不可恢复,你确定要删除吗?", + }, + newDesktopModal: { + error: { + tooManyTitle: "新建桌面失败", + tooManyContent: "该集群桌面数目达到最大限制", + }, + modal: { + createNewDesktop: "新建桌面", + loginNode: "登录节点", + wm: "桌面", + desktopName: "桌面名", + }, + }, + }, + // app + app: { + appSessionTable: { + table: { + sessionId: "作业名", + jobId: "作业ID", + appId: "应用", + submitTime: "提交时间", + state: "状态", + remainingTime: "剩余时间", + popFinishConfirmTitle: "确定结束这个任务吗", + popFinishConfirmMessage: "任务结束请求已经提交", + popCancelConfirmTitle: "确定取消这个任务吗", + popCancelConfirmMessage: "任务取消请求已经提交", + linkToPath: "进入目录", + }, + filterForm: { + appJobName: "作业名", + autoRefresh: "10s自动刷新", + onlyNotEnded: "只展示未结束的作业", + }, + }, + connectToAppLink: { + notFoundMessage: "此应用会话不存在", + notConnectableMessage: "此应用目前无法连接", + notReady: "应用还未准备好", + connect: "连接", + }, + createApps: { + notFoundMessage: "没有可以创建的交互式应用", + loading: "正在加载可创建的交互式应用", + create: "创建", + }, + launchAppForm: { + errorMessage: "创建应用失败", + successMessage: "创建成功", + loading: "查询上次提交记录中", + appJobName: "作业名", + account: "账户", + partition: "分区", + qos: "QOS", + nodeCount: "节点数", + gpuCount: "单节点GPU卡数", + coreCount: "单节点CPU核心数", + maxTime: "最长运行时间", + minute: "分钟", + totalGpuCount: "总GPU卡数", + totalCpuCount: "总CPU核心数", + totalMemory: "总内存容量", + appCommentTitle: "说明", + }, + }, + }, + component:{ + errorPages:{ + notAllowedPage:"不允许访问此页面", + systemNotAllowed:"系统不允许您访问此页面。", + notAllowed:"不允许访问", + needLogin:"需要登录", + notLogin:"您未登录或者登录状态已经过期。您需要登录才能访问此页面。", + + login:"登录", + notExist:"不存在", + pageNotExist:"您所请求的页面不存在。", + serverWrong:"服务器出错", + sorry:"对不起,服务器出错。请刷新重试。", + }, + others:{ + clusterSelector: "请选择集群", + }, + }, + pages: { + apps: { + create: { + title: "创建", + error404: "此应用不存在", + }, + createApps: { + subTitle: "您所请求的集群不存在", + title: "创建应用", + pageTitle: "在{}集群创建应用", + }, + sessions: { + subTitle: "您所请求的集群不存在", + title: "交互式应用", + pageTitle: "集群{}交互式应用", + }, + }, + desktop: { + title: "桌面", + pageTitle: "登录节点上的桌面", + }, + files: { + path: { + subTitle: "您所请求的集群不存在", + title: "文件管理", + }, + fileTransfer: { + confirmTransferTitle: "确认开始传输?", + confirmTransferContent: "确认从 {} 传输到 {} 吗?", + confirmOk: "确认", + transferStartInfo: "传输任务已经开始", + transferTitle: "跨集群文件传输", + }, + currentTransferInfo: { + checkTransfer: "文件传输进度查看", + }, + }, + jobs: { + allJobs: { + title: "历史作业", + pageTitle: "本用户所有历史作业", + }, + runningJobs: { + title: "未结束的作业", + pageTitle: "本用户未结束的作业", + }, + savedJobs: { + title: "作业模板", + pageTitle: "作业模板列表", + }, + submit: { + title: "提交作业", + pageTitle: "提交作业", + spin: "正在加载作业模板", + }, + }, + profile: { + title: "账号信息", + userInfo: "用户信息", + identityId: "用户ID", + name: "用户姓名", + changePassword: "修改密码", + loginPassword: "登录密码", + }, + shell: { + loginNode: { + title: "的终端", + content: "以ID: {} 连接到集群 {} 的 {} 节点", + reloadButton: "刷新并重新连接", + popoverTitle: "命令", + popoverContent1: "跳转到文件系统", + popoverContent2: ",输入该命令后会跳转到文件系统,您可以进行文件的上传和下载", + + popoverContent3: "文件下载", + popoverContentFile:"文件名", + popoverContent4: ",输入", + popoverContent5: ",您当前路径下的该文件会被下载到本地,目前不支持输入相对路径,", + + popoverContent6: "如果需要下载其他目录下的文件请使用", + popoverContent7: "命令跳转到文件系统。", + popoverContent8: "使用示例:", + command:"命令", + }, + index: { + title: "终端", + content: "启动以下集群的终端:", + }, + }, + _app: { + textExceedsLength:"终端登录欢迎提示信息过多,请减少不必要的信息输出!", + sshError:"无法以用户身份连接到登录节点。请确认您的家目录的权限为700、750或者755", + sftpError:"SFTP操作失败,请确认您是否有操作的权限", + otherError:"服务器出错啦!", + }, + dashboard: { + title: "仪表盘", + }, + }, +}; diff --git a/apps/ai/src/layouts/AntdConfigProvider.tsx b/apps/ai/src/layouts/AntdConfigProvider.tsx new file mode 100644 index 0000000000..453fab42ee --- /dev/null +++ b/apps/ai/src/layouts/AntdConfigProvider.tsx @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import "dayjs/locale/zh-cn"; + +import { App, ConfigProvider, theme } from "antd"; +import zhCNlocale from "antd/locale/zh_CN"; +import React, { } from "react"; +import { AppFloatButtons } from "src/layouts/AppFloatButtons"; +import { useDarkMode } from "src/layouts/darkMode"; +import { ThemeProvider } from "styled-components"; + + +type Props = React.PropsWithChildren<{ + color: string | undefined; +}>; + +const StyledComponentsThemeProvider: React.FC = ({ children }) => { + const { token } = theme.useToken(); + + return ( + + {children} + + ); +}; + +export const AntdConfigProvider: React.FC = ({ children, color }) => { + + const { dark } = useDarkMode(); + + return ( + + + + + {children} + + + + ); +}; diff --git a/apps/ai/src/layouts/AppFloatButtons.tsx b/apps/ai/src/layouts/AppFloatButtons.tsx new file mode 100644 index 0000000000..36c1d9318b --- /dev/null +++ b/apps/ai/src/layouts/AppFloatButtons.tsx @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { FloatButton } from "antd"; +import { DarkModeButton } from "src/layouts/darkMode"; + +export const AppFloatButtons = () => { + return ( + + + + ); +}; + + diff --git a/apps/ai/src/layouts/base/BaseLayout.tsx b/apps/ai/src/layouts/base/BaseLayout.tsx new file mode 100644 index 0000000000..a186ec5f4a --- /dev/null +++ b/apps/ai/src/layouts/base/BaseLayout.tsx @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Footer } from "@scow/lib-web/build/layouts/base/Footer"; +import { Grid, Layout } from "antd"; +import { usePathname } from "next/navigation"; +import React, { PropsWithChildren, useMemo, useState } from "react"; +import { Header } from "src/layouts/base/header"; +import { match } from "src/layouts/base/matchers"; +import { NavItemProps } from "src/layouts/base/NavItemProps"; +import { SideNav } from "src/layouts/base/SideNav"; +import { ClientUserInfo } from "src/server/trpc/route/auth"; +import { arrayContainsElement } from "src/utils/array"; +import { trpc } from "src/utils/trpc"; +import { styled } from "styled-components"; + +// import logo from "src/assets/logo-no-text.svg"; +const { useBreakpoint } = Grid; + +const Root = styled.div` + min-height: 100vh; + display: flex; + flex-direction: column; +`; + +const ContentPart = styled.div` + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + overflow: hidden; +`; + +const Content = styled(Layout.Content)` + margin: 8px; + padding: 16px; + flex: 1; + background: ${({ theme }) => theme.token.colorBgLayout}; +`; + +const StyledLayout = styled(Layout)` + position: relative; +`; + +type Props = PropsWithChildren<{ + routes?: NavItemProps[]; + user?: ClientUserInfo | undefined; + headerRightContent?: React.ReactNode; + footerText?: string; + versionTag?: string | undefined; +}>; + +export const BaseLayout: React.FC> = ({ + routes = [], children, user = undefined, headerRightContent, versionTag, footerText, +}) => { + + const pathname = usePathname() ?? ""; + + const [sidebarCollapsed, setSidebarCollapsed] = useState(true); + + // get the first level route + const firstLevelRoute = useMemo(() => routes.find((x) => match(x, pathname)), [routes, pathname]); + + const { md } = useBreakpoint(); + + const sidebarRoutes = md ? firstLevelRoute?.children : routes; + + const hasSidebar = arrayContainsElement(sidebarRoutes); + + const useLogoutMutation = trpc.auth.logout.useMutation(); + + return ( + +
{ useLogoutMutation.mutateAsync().then(() => { location.reload(); }); }} + userLinks={[]} + languageId="zh_cn" + right={headerRightContent} + /> + + { + (hasSidebar) ? ( + + ) : undefined + } + + + {children} + +
+ + + + ); +}; + diff --git a/apps/ai/src/layouts/base/FormLayout.tsx b/apps/ai/src/layouts/base/FormLayout.tsx new file mode 100644 index 0000000000..ac609f7e48 --- /dev/null +++ b/apps/ai/src/layouts/base/FormLayout.tsx @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import React from "react"; +import { styled } from "styled-components"; + +export const FormContainer = styled.div<{ maxWidth: number }>` + display: flex; + flex-direction: column; + justify-content: center; + max-width: ${({ maxWidth }) => maxWidth}px; + flex: 1; +`; + +export const ChildrenContainer = styled.div` + margin: 16px 0; +`; + +type Props = React.PropsWithChildren<{ + maxWidth?: number; +}>; + +export const FormLayout: React.FC = ({ + children, + maxWidth = 600, +}) => { + return ( + + + {children} + + + ); +}; diff --git a/apps/ai/src/layouts/base/NavItemProps.d.ts b/apps/ai/src/layouts/base/NavItemProps.d.ts new file mode 100644 index 0000000000..bc7fdc6927 --- /dev/null +++ b/apps/ai/src/layouts/base/NavItemProps.d.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import React from "react"; + +export interface NavItemProps { + path: string; + clickToPath?: string; + text: string; + Icon: React.ForwardRefExoticComponent<{}>; + match?: (spec: string, pathname: string) => boolean; + children?: NavItemProps[]; + clickable?: boolean; + openInNewPage?: boolean; + handleClick?: () => void; +} diff --git a/apps/ai/src/layouts/base/SideNav/BodyMask.tsx b/apps/ai/src/layouts/base/SideNav/BodyMask.tsx new file mode 100644 index 0000000000..48230d469b --- /dev/null +++ b/apps/ai/src/layouts/base/SideNav/BodyMask.tsx @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { styled } from "styled-components"; + +interface Props { + sidebarShown: boolean; + breakpoint: number; + onClick: () => void; +} + +type MaskProps = Pick; + +const Mask = styled.div` + position: absolute; + left: 0; + right: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.35); + z-index:2; + + display: none; + + @media (max-width: ${(props: MaskProps) => props.breakpoint}px) { + display: ${(props: MaskProps) => props.sidebarShown ? "initial" : "none"}; + } +`; + +export default function BodyMask(props: Props) { + + return ( + + ); +} diff --git a/apps/ai/src/layouts/base/SideNav/index.tsx b/apps/ai/src/layouts/base/SideNav/index.tsx new file mode 100644 index 0000000000..969210f5ca --- /dev/null +++ b/apps/ai/src/layouts/base/SideNav/index.tsx @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Layout, Menu } from "antd"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { calcSelectedKeys, createMenuItems } from "src/layouts/base/common"; +import { antdBreakpoints } from "src/layouts/base/constants"; +import { arrayContainsElement } from "src/utils/array"; +import { useDidUpdateEffect } from "src/utils/hooks"; +import { styled } from "styled-components"; + +import { NavItemProps } from "../NavItemProps"; +import BodyMask from "./BodyMask"; + +const { Sider } = Layout; + +const breakpoint = "lg"; + +interface Props { + collapsed: boolean; + setCollapsed: (collapsed: boolean) => void; + + routes: NavItemProps[]; + pathname: string; + +} + +const StyledSider = styled(Sider)` + + @media (max-width: ${antdBreakpoints[breakpoint]}px ) { + position: absolute !important; + z-index: 1000; + + body, html { + overflow-x: hidden; + overflow-y: auto; + } + + overflow: auto; + } + + height: 100%; + + + .ant-menu-item:first-child { + margin-top: 0px; + } +`; + +const Container = styled.div` + .ant-layout-sider { + background: initial; + } +`; + + +function getAllParentKeys(routes: NavItemProps[]): string[] { + return routes.map((x) => { + if (arrayContainsElement(x.children)) { + return [...getAllParentKeys(x.children), x.path]; + } else { + return []; + } + }).flat(); +} + +export const SideNav: React.FC = ({ + collapsed, routes, setCollapsed, pathname, +}) => { + + const parentKeys = useMemo(() => getAllParentKeys(routes), [routes]); + + const [openKeys, setOpenKeys] = useState(parentKeys); + + useDidUpdateEffect(() => { + setOpenKeys(parentKeys); + }, [parentKeys]); + + const onBreakpoint = useCallback((broken: boolean) => { + // if broken, big to small. collapse the sidebar + // if not, small to big, expand the sidebar + setCollapsed(broken); + }, [setCollapsed]); + + const selectedKeys = useMemo(() => calcSelectedKeys(routes, pathname), [routes, pathname]); + + useEffect(() => { + if (window.innerWidth <= antdBreakpoints[breakpoint]) { + setCollapsed(true); + } + }, [pathname]); + + if (!arrayContainsElement(routes)) { + return null; + } + return ( + + setCollapsed(true)} + sidebarShown={!collapsed} + breakpoint={antdBreakpoints[breakpoint]} + /> + + + + + + ); +}; + diff --git a/apps/ai/src/layouts/base/common.tsx b/apps/ai/src/layouts/base/common.tsx new file mode 100644 index 0000000000..2bf2d409ce --- /dev/null +++ b/apps/ai/src/layouts/base/common.tsx @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { ItemType } from "antd/es/menu/hooks/useItems"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { match } from "src/layouts/base/matchers"; +import { NavItemProps } from "src/layouts/base/NavItemProps"; +import { arrayContainsElement } from "src/utils/array"; + +export const EXTERNAL_URL_PREFIX = ["http://", "https://"]; + +export function createMenuItems( + routes: NavItemProps[], + parentClickable: boolean, +) { + + const router = useRouter(); + + function createMenuItem(route: NavItemProps): ItemType { + if (arrayContainsElement(route.children)) { + return { + icon: , + key: route.path, + title: route.text, + label: route.text, + onTitleClick:(route.clickable ?? parentClickable) + ? () => { + const target = route.clickToPath ?? route.path; + route.handleClick?.(); + if (route.openInNewPage) { + window.open(target); + } else { + EXTERNAL_URL_PREFIX.some((pref) => target.startsWith(pref)) + ? window.location.href = target : router.push(target); + } + } + : undefined, + children: createMenuItems(route.children, parentClickable), + } as ItemType; + } + + return { + icon: , + key: route.path, + label: ( + + {route.text} + + ), + onClick: () => { + route.handleClick?.(); + }, + } as ItemType; + } + + const items = routes.map((r) => createMenuItem(r)); + + return items; +} + +export function calcSelectedKeys(links: NavItemProps[], pathname: string) { + + return links.reduce((prev, curr) => { + if (arrayContainsElement(curr.children)) { + prev.push(...calcSelectedKeys(curr.children, pathname)); + } + if ( + (curr.path === "/" && pathname === "/") || + (curr.path !== "/" && match(curr, pathname)) + ) { + prev.push(curr.path); + } + + return prev; + }, [] as string[]); +} diff --git a/apps/ai/src/layouts/base/constants.ts b/apps/ai/src/layouts/base/constants.ts new file mode 100644 index 0000000000..59723bce5d --- /dev/null +++ b/apps/ai/src/layouts/base/constants.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export const antdBreakpoints = { + xxs: 0, + xs: 480, + sm: 576, + md: 768, + lg: 992, + xl: 1200, + xxl: 1600, +}; + +export type Breakpoint = keyof typeof antdBreakpoints; + +export const layoutConstants = { + paddingBreakpoint: "md" as Breakpoint, + menuBreakpoint: "md" as Breakpoint, + headerHeight: 56, + sidebarBreakpoint: "lg" as Breakpoint, + headerIconColor: "#ffffff", + headerIconBackgroundColor: "#1890FF", + headerBackgrounColor: "#001529", + maxWidth: 1200, +}; + diff --git a/apps/ai/src/layouts/base/header/BigScreenMenu.tsx b/apps/ai/src/layouts/base/header/BigScreenMenu.tsx new file mode 100644 index 0000000000..7af4af01c4 --- /dev/null +++ b/apps/ai/src/layouts/base/header/BigScreenMenu.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Menu } from "antd"; +import React, { useMemo } from "react"; +import { calcSelectedKeys, createMenuItems } from "src/layouts/base/common"; +import { antdBreakpoints } from "src/layouts/base/constants"; +import { NavItemProps } from "src/layouts/base/NavItemProps"; +import { arrayContainsElement } from "src/utils/array"; +import { styled } from "styled-components"; + +const Container = styled.div` + + @media (max-width: ${antdBreakpoints.md}px) { + display: none; + } + + width: 100%; + + .ant-menu-item { + padding-left: 16px !important; + } +`; + +interface Props { + routes?: NavItemProps[]; + className?: string; + pathname: string; +} +export const BigScreenMenu: React.FC = ({ + routes, className, pathname, +}) => { + + const selectedKeys = useMemo(() => + routes + ? calcSelectedKeys(routes, pathname) + : [] + , [routes, pathname]); + + return ( + + { + arrayContainsElement(routes) + ? ( + + ) : undefined + } + + ); +}; diff --git a/apps/ai/src/layouts/base/header/Components.tsx b/apps/ai/src/layouts/base/header/Components.tsx new file mode 100644 index 0000000000..16a3592fa9 --- /dev/null +++ b/apps/ai/src/layouts/base/header/Components.tsx @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { UserInfo } from "@scow/lib-web/build/layouts/base/types"; +import { Typography } from "antd"; +import { join } from "path"; +import { antdBreakpoints } from "src/layouts/base/constants"; +import { styled } from "styled-components"; + +export const HeaderItem = styled.div` + padding: 0 16px; + /* justify-content: center; */ + height: 100%; + + @media (max-width: ${antdBreakpoints.md}px) { + padding-right: 4px; + } + +`; + +export const HiddenOnSmallScreenSpan = styled.span` + @media (max-width: ${antdBreakpoints.md}px) { + display: none; + } +`; + +interface JumpToAnotherLinkProps { + user: UserInfo | undefined; + icon: React.ReactNode; + link: string | undefined; + linkText: string; +} + +export const JumpToAnotherLink: React.FC = ({ user, link, icon, linkText }) => { + if (!link) { return null; } + + return ( + + + {/* Cannot use Link because links adds BASE_PATH, but MIS_URL already contains it */} + { + + const { dark } = useDarkMode(); + const query = new URLSearchParams({ type: "logo", preferDark: dark ? "true" : "false" }).toString(); + + const { data } = trpc.config.publicConfig.useQuery(); + + return ( + + + { + data ? ( + logo + ) : undefined + } + + + ); +}; diff --git a/apps/ai/src/layouts/base/header/UserIndicator.tsx b/apps/ai/src/layouts/base/header/UserIndicator.tsx new file mode 100644 index 0000000000..aeef70aad0 --- /dev/null +++ b/apps/ai/src/layouts/base/header/UserIndicator.tsx @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; +import { DownOutlined, UserOutlined } from "@ant-design/icons"; +import { EXTERNAL_URL_PREFIX } from "@scow/lib-web/build/layouts/base/common"; +import { UserInfo, UserLink } from "@scow/lib-web/build/layouts/base/types"; +import { getCurrentLangLibWebText } from "@scow/lib-web/build/utils/libWebI18n/libI18n"; +import { Dropdown, Typography } from "antd"; +import Link from "next/link"; +import React from "react"; +import { antdBreakpoints } from "src/layouts/base/constants"; +import { styled } from "styled-components"; + +interface Props { + user: UserInfo | undefined; + logout: (() => void) | undefined; + userLinks?: UserLink[]; + languageId: string; +} + +const Container = styled.div` + white-space: nowrap; +`; + +const InlineBlockA = styled.a` + cursor: pointer; + line-height: 45px; + display: inline-block; +`; + +const HiddenOnSmallScreen = styled.span` + @media (max-width: ${antdBreakpoints.md}px) { + display: none; + } +`; + +export const UserIndicator: React.FC = ({ + user, logout, userLinks, languageId, +}) => { + + return ( + + { + user ? ( + + {getCurrentLangLibWebText(languageId, "userIndicatorInfo")} + }, + ...userLinks ? userLinks.map((link) => { + return ({ + key: link.text, + label: EXTERNAL_URL_PREFIX.some((pref) => link.url.startsWith(pref)) ? ( + {link.text} + ) : ( + {link.text} + ), + }); + }) : [], + { key: "logout", + onClick: logout, + label: getCurrentLangLibWebText(languageId, "userIndicatorLogout") }, + ], + }} + > + + + + {user.name ?? user.identityId} + + + + + ) : ( + + {getCurrentLangLibWebText(languageId, "userIndicatorLogin")} + + ) + } + + ); +}; diff --git a/apps/ai/src/layouts/base/header/index.tsx b/apps/ai/src/layouts/base/header/index.tsx new file mode 100644 index 0000000000..67af8e476b --- /dev/null +++ b/apps/ai/src/layouts/base/header/index.tsx @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons"; +import { UserLink } from "@scow/lib-web/build/layouts/base/types"; +import { Space } from "antd"; +import React from "react"; +import { antdBreakpoints } from "src/layouts/base/constants"; +import { BigScreenMenu } from "src/layouts/base/header/BigScreenMenu"; +import { Logo } from "src/layouts/base/header/Logo"; +import { NavItemProps } from "src/layouts/base/NavItemProps"; +import { ClientUserInfo } from "src/server/trpc/route/auth"; +import { styled } from "styled-components"; + +import { UserIndicator } from "./UserIndicator"; + +interface ComponentProps { + homepage?: boolean; +} + +const Container = styled.header` + display: flex; + padding: 0 4px; + box-shadow: ${({ theme }) => theme.token.boxShadow }; + z-index: 50; + align-items: center; + background-color: ${({ theme }) => theme.token.colorBgContainer}; +`; + +const HeaderItem = styled.div` + padding: 0 16px; + /* justify-content: center; */ + height: 100%; +`; + +const MenuPart = styled(HeaderItem)` + flex: 1; + min-width: 0; +`; + +const MenuPartPlaceholder = styled.div` + flex: 1; + @media (min-width: ${antdBreakpoints.md}px) { + display: none; + } +`; + +const IndicatorPart = styled(HeaderItem)` + justify-self: flex-end; + flex-wrap: nowrap; +`; + +interface Props { + hasSidebar: boolean; + setSidebarCollapsed: (collapsed: boolean) => void; + sidebarCollapsed: boolean; + routes?: NavItemProps[]; + logout: (() => void) | undefined; + user: ClientUserInfo | undefined; + userLinks?: UserLink[]; + pathname: string; + languageId: string, + right?: React.ReactNode; +} + +export const Header: React.FC = ({ + hasSidebar, routes, + setSidebarCollapsed, + sidebarCollapsed, + logout, + user, + pathname, + userLinks, + languageId, + right, +}) => { + + return ( + + + + {hasSidebar + ? ( + setSidebarCollapsed(!sidebarCollapsed)}> + {React.createElement( + sidebarCollapsed ? MenuUnfoldOutlined : MenuFoldOutlined)} + + ) : undefined + } + + + + + + + + {right} + + + + + + ); +}; diff --git a/apps/ai/src/layouts/base/matchers.ts b/apps/ai/src/layouts/base/matchers.ts new file mode 100644 index 0000000000..d279095132 --- /dev/null +++ b/apps/ai/src/layouts/base/matchers.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import type { NavItemProps } from "src/layouts/base/NavItemProps"; + +export type Matcher = (spec: string, path: string) => boolean; + +const removeQuery = (path: string) => path.split("?", 1)[0]; + +export const exactMatch: Matcher = (spec, path) => { + return path === spec; +}; + +export const startsWithMatch: Matcher = (spec, path) => { + const normalizedPath = path.endsWith("/") ? path.substring(0, path.length - 1) : path; + // avoid /test matches /test-test + return normalizedPath === spec || normalizedPath.startsWith(spec + "/"); +}; + +export const match = (item: NavItemProps, path: string) => { + return (item.match ?? startsWithMatch)(item.path, removeQuery(path)); +}; diff --git a/apps/ai/src/layouts/constants.ts b/apps/ai/src/layouts/constants.ts new file mode 100644 index 0000000000..5a47b969d1 --- /dev/null +++ b/apps/ai/src/layouts/constants.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export const PRIMARY_COLOR = "#9a0000"; + diff --git a/apps/ai/src/layouts/darkMode.tsx b/apps/ai/src/layouts/darkMode.tsx new file mode 100644 index 0000000000..c2dfc91164 --- /dev/null +++ b/apps/ai/src/layouts/darkMode.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { FloatButton } from "antd"; +import Image from "next/image"; +import { setCookie } from "nookies"; +import React, { PropsWithChildren, useEffect, useState } from "react"; +import moon from "src/components/icons/moon.svg"; +import sun from "src/components/icons/sun.svg"; +import sunMoon from "src/components/icons/sun-moon.svg"; + +const modes = ["system", "dark", "light"] as const; + +const icons = { + system: [sunMoon, "system", "跟随系统"], + light: [sun, "light", "亮色"], + dark: [moon, "dark", "暗色"], +}; + +export type DarkMode = typeof modes[number]; + +const DarkModeContext = React.createContext<{ + mode: DarkMode; + dark: boolean; + setMode: (mode: DarkMode) => void; + }>(undefined!); + +export const useDarkMode = () => React.useContext(DarkModeContext); + +export const DarkModeButton = () => { + const { mode, setMode } = useDarkMode(); + + const [icon, alt, label] = icons[mode]; + + return ( + setMode(mode === "system" ? "dark" : mode === "dark" ? "light" : "system")} + icon={} + tooltip={label} + // icon={icon} + /> + ); +}; + +export interface DarkModeCookie { + dark: boolean; + mode: DarkMode; +} + +interface Props { + initial?: DarkModeCookie; +} + +export const DARK_MODE_COOKIE_NAME = "xscow-dark"; + +export const DarkModeProvider = ({ children, initial }: PropsWithChildren) => { + + const [mode, setMode] = useState(initial?.mode ?? "system"); + + const [dark, setDark] = useState(initial?.dark ?? false); + + useEffect(() => { + setCookie(null, DARK_MODE_COOKIE_NAME, JSON.stringify({ mode, dark } as DarkModeCookie), { + maxAge: 30 * 24 * 60 * 60, + path: "/", + }); + }, [dark, mode]); + + useEffect(() => { + if (mode === "system") { + + const onChange = function(this: MediaQueryList, ev: MediaQueryListEvent) { + setDark(ev.matches); + }; + + const media = window.matchMedia("(prefers-color-scheme: dark)"); + + setDark(media.matches); + + media.addEventListener("change", onChange); + + return () => media.removeEventListener("change", onChange); + } else { + setDark(mode === "dark"); + } + }, [mode]); + + return ( + + {children} + + + ); +}; diff --git a/apps/ai/src/layouts/error/ForbiddenPage.tsx b/apps/ai/src/layouts/error/ForbiddenPage.tsx new file mode 100644 index 0000000000..13ba6b84d1 --- /dev/null +++ b/apps/ai/src/layouts/error/ForbiddenPage.tsx @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Result } from "antd"; +import React from "react"; +import { Head } from "src/utils/head"; + +interface Props { + title?: React.ReactNode; + subTitle?: React.ReactNode; +} + +export const ForbiddenPage: React.FC = ({ + title = "不允许访问此页面", + subTitle = "系统不允许您访问此页面。", +}) => { + return ( + <> + + + + ); +}; diff --git a/apps/ai/src/layouts/error/NotAuthorizedPage.tsx b/apps/ai/src/layouts/error/NotAuthorizedPage.tsx new file mode 100644 index 0000000000..fceb66831b --- /dev/null +++ b/apps/ai/src/layouts/error/NotAuthorizedPage.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { Button, Result } from "antd"; +import Link from "next/link"; +import { Head } from "src/utils/head"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const NotAuthorizedPage = () => { + + return ( + <> + + + + + )} + /> + + ); +}; diff --git a/apps/ai/src/layouts/error/NotFoundPage.tsx b/apps/ai/src/layouts/error/NotFoundPage.tsx new file mode 100644 index 0000000000..e33ab0d11a --- /dev/null +++ b/apps/ai/src/layouts/error/NotFoundPage.tsx @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Result } from "antd"; +import { Head } from "src/utils/head"; + +export const NotFoundPage = () => { + return ( + <> + + + + ); +}; diff --git a/apps/ai/src/layouts/error/RootErrorContent.tsx b/apps/ai/src/layouts/error/RootErrorContent.tsx new file mode 100644 index 0000000000..ed184a533f --- /dev/null +++ b/apps/ai/src/layouts/error/RootErrorContent.tsx @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { ErrorBoundaryContentProps } from "src/components/ErrorBoundary"; +import { BaseLayout } from "src/layouts/base/BaseLayout"; +import { ServerErrorPage } from "src/layouts/error/ServerErrorPage"; + +export const RootErrorContent: React.FC = () => { + + return ( + + + + ); +}; diff --git a/apps/ai/src/layouts/error/ServerErrorPage.tsx b/apps/ai/src/layouts/error/ServerErrorPage.tsx new file mode 100644 index 0000000000..c593ac4afa --- /dev/null +++ b/apps/ai/src/layouts/error/ServerErrorPage.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { Result } from "antd"; +import React from "react"; +import { Head } from "src/utils/head"; + +export const ServerErrorPage: React.FC = () => { + return ( + <> + + + + ); +}; diff --git a/apps/ai/src/layouts/styleRegistry/AntdRegistry.tsx b/apps/ai/src/layouts/styleRegistry/AntdRegistry.tsx new file mode 100644 index 0000000000..efc7ed622d --- /dev/null +++ b/apps/ai/src/layouts/styleRegistry/AntdRegistry.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +"use client"; + +import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs"; +import { useServerInsertedHTML } from "next/navigation"; +import { useState } from "react"; + +export function AntdStyleRegistry({ children }: { children: React.ReactNode }) { + const [cache] = useState(() => createCache()); + + const render = <>{children}; + + useServerInsertedHTML(() => { + return ( +