From 835e22457b23e9be7700a11734d41c694231818a Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Thu, 9 May 2024 10:27:48 +0800 Subject: [PATCH] feat: add providers workorder app (#4718) * feat: add providers workorder app Signed-off-by: jingyang <3161362058@qq.com> * Optimized Notification Signed-off-by: jingyang <3161362058@qq.com> * Support link jump Signed-off-by: jingyang <3161362058@qq.com> * fix env * fix user token * supprt fastgpt * done * set interval * delete session * fix i18n * add Markdown * delete unused file * add api 401 * fix feishu * add region * fix 404 * add ci --------- Signed-off-by: jingyang <3161362058@qq.com> --- .github/workflows/frontend.yml | 2 + frontend/Makefile | 3 +- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 381 +++++++++--- frontend/providers/workorder/.env.template | 15 + frontend/providers/workorder/.eslintrc.json | 6 + frontend/providers/workorder/.gitignore | 41 ++ frontend/providers/workorder/.prettierignore | 4 + frontend/providers/workorder/.prettierrc.js | 21 + frontend/providers/workorder/README.md | 14 + frontend/providers/workorder/deploy/Kubefile | 20 + .../deploy/manifests/appcr.yaml.tmpl | 21 + .../deploy/manifests/deploy.yaml.tmpl | 90 +++ .../deploy/manifests/ingress.yaml.tmpl | 51 ++ frontend/providers/workorder/i18nrc.json | 8 + .../workorder/next-i18next.config.js | 12 + frontend/providers/workorder/next.config.js | 28 + frontend/providers/workorder/package.json | 75 +++ .../providers/workorder/public/favicon.ico | Bin 0 -> 4286 bytes .../workorder/public/icons/download.svg | 3 + .../workorder/public/icons/sealos.svg | 1 + .../workorder/public/icons/token.svg | 10 + .../workorder/public/icons/wechat.svg | 4 + .../workorder/public/locales/en/common.json | 64 ++ .../workorder/public/locales/zh/common.json | 64 ++ frontend/providers/workorder/public/logo.svg | 9 + .../providers/workorder/src/api/platform.ts | 35 ++ frontend/providers/workorder/src/api/user.ts | 9 + .../providers/workorder/src/api/workorder.ts | 30 + .../src/components/AppStatusTag/index.tsx | 38 ++ .../src/components/FileSelect/index.tsx | 255 ++++++++ .../src/components/Icon/icons/analyze.svg | 1 + .../src/components/Icon/icons/arrowLeft.svg | 1 + .../src/components/Icon/icons/change.svg | 1 + .../src/components/Icon/icons/close.svg | 3 + .../src/components/Icon/icons/connection.svg | 1 + .../src/components/Icon/icons/continue.svg | 1 + .../src/components/Icon/icons/copy.svg | 1 + .../components/Icon/icons/defaultAvator.svg | 5 + .../src/components/Icon/icons/delete.svg | 1 + .../src/components/Icon/icons/detail.svg | 3 + .../src/components/Icon/icons/download.svg | 1 + .../src/components/Icon/icons/formInfo.svg | 1 + .../src/components/Icon/icons/info.svg | 1 + .../src/components/Icon/icons/link.svg | 4 + .../src/components/Icon/icons/log.svg | 1 + .../src/components/Icon/icons/logo.svg | 3 + .../src/components/Icon/icons/more.svg | 1 + .../src/components/Icon/icons/noEvents.svg | 1 + .../src/components/Icon/icons/pause.svg | 1 + .../src/components/Icon/icons/plus.svg | 1 + .../src/components/Icon/icons/podList.svg | 1 + .../src/components/Icon/icons/read.svg | 1 + .../src/components/Icon/icons/restart.svg | 1 + .../src/components/Icon/icons/restore.svg | 1 + .../src/components/Icon/icons/robot.svg | 3 + .../components/Icon/icons/sealosAvator.svg | 6 + .../src/components/Icon/icons/settings.svg | 1 + .../src/components/Icon/icons/start.svg | 1 + .../components/Icon/icons/statusDetail.svg | 1 + .../src/components/Icon/icons/terminal.svg | 1 + .../src/components/Icon/icons/unread.svg | 1 + .../src/components/Icon/icons/warning.svg | 1 + .../workorder/src/components/Icon/index.tsx | 51 ++ .../src/components/Markdown/CodeLight.tsx | 345 +++++++++++ .../src/components/Markdown/img/Image.tsx | 74 +++ .../src/components/Markdown/index.module.scss | 424 +++++++++++++ .../src/components/Markdown/index.tsx | 77 +++ .../workorder/src/components/Menu/index.tsx | 50 ++ .../src/components/MonitorTabs/index.tsx | 97 +++ .../src/components/MyTooltip/index.tsx | 24 + .../src/components/Pagination/index.tsx | 132 ++++ .../src/components/RangeDate/index.tsx | 70 +++ .../components/RangeDate/style.module.scss | 0 .../workorder/src/components/Select/index.tsx | 117 ++++ .../src/components/SelectDateRange/index.tsx | 289 +++++++++ .../workorder/src/components/Table/index.tsx | 71 +++ .../workorder/src/components/Tabs/index.tsx | 94 +++ .../workorder/src/components/Tip/index.tsx | 66 ++ .../workorder/src/constants/theme.ts | 334 ++++++++++ .../workorder/src/constants/workorder.ts | 85 +++ .../workorder/src/hooks/useConfirm.tsx | 79 +++ .../workorder/src/hooks/useLoading.tsx | 34 + .../workorder/src/hooks/useScreen.ts | 25 + .../workorder/src/hooks/useSelectFile.tsx | 44 ++ .../providers/workorder/src/hooks/useStore.ts | 17 + .../providers/workorder/src/hooks/useToast.ts | 12 + .../providers/workorder/src/pages/404.tsx | 13 + .../providers/workorder/src/pages/_app.tsx | 147 +++++ .../workorder/src/pages/_document.tsx | 21 + .../workorder/src/pages/api/ai/fastgpt.ts | 87 +++ .../workorder/src/pages/api/auth/desktop.ts | 66 ++ .../workorder/src/pages/api/auth/findById.ts | 42 ++ .../workorder/src/pages/api/minio/delete.ts | 43 ++ .../workorder/src/pages/api/minio/getUrl.ts | 43 ++ .../workorder/src/pages/api/minio/upload.ts | 127 ++++ .../src/pages/api/platform/feishu.ts | 107 ++++ .../src/pages/api/platform/getEnv.ts | 15 + .../src/pages/api/workorder/create.ts | 52 ++ .../src/pages/api/workorder/delete.ts | 31 + .../src/pages/api/workorder/getByID.ts | 36 ++ .../src/pages/api/workorder/listByUser.ts | 50 ++ .../src/pages/api/workorder/update.ts | 33 + .../src/pages/api/workorder/updateDialog.ts | 56 ++ .../providers/workorder/src/pages/index.tsx | 22 + .../create/components/ErrorModal.tsx | 41 ++ .../workorder/create/components/Header.tsx | 44 ++ .../src/pages/workorder/create/index.tsx | 214 +++++++ .../detail/components/AppBaseInfo.tsx | 178 ++++++ .../detail/components/AppMainInfo.tsx | 580 ++++++++++++++++++ .../workorder/detail/components/Header.tsx | 104 ++++ .../src/pages/workorder/detail/index.tsx | 132 ++++ .../src/pages/workorders/components/List.tsx | 179 ++++++ .../workorder/src/pages/workorders/index.tsx | 180 ++++++ .../workorder/src/services/backend/auth.ts | 47 ++ .../src/services/backend/kubernetes.ts | 276 +++++++++ .../src/services/backend/response.ts | 36 ++ .../workorder/src/services/db/mongodb.ts | 24 + .../workorder/src/services/db/region.ts | 14 + .../workorder/src/services/db/user.ts | 32 + .../workorder/src/services/db/workorder.ts | 156 +++++ .../providers/workorder/src/services/error.ts | 19 + .../workorder/src/services/kubernet.ts | 50 ++ .../workorder/src/services/request.ts | 95 +++ frontend/providers/workorder/src/store/env.ts | 32 + .../providers/workorder/src/store/global.ts | 37 ++ .../providers/workorder/src/store/session.ts | 40 ++ .../providers/workorder/src/styles/reset.scss | 151 +++++ .../providers/workorder/src/types/index.ts | 5 + .../providers/workorder/src/types/region.ts | 5 + .../providers/workorder/src/types/user.ts | 24 + .../workorder/src/types/workorder.ts | 56 ++ .../workorder/src/utils/cookieUtils.ts | 15 + .../providers/workorder/src/utils/crypto.ts | 7 + .../providers/workorder/src/utils/file.ts | 77 +++ .../providers/workorder/src/utils/i18n.ts | 11 + .../providers/workorder/src/utils/tools.ts | 292 +++++++++ frontend/providers/workorder/tsconfig.json | 21 + 138 files changed, 8045 insertions(+), 99 deletions(-) create mode 100644 frontend/providers/workorder/.env.template create mode 100644 frontend/providers/workorder/.eslintrc.json create mode 100644 frontend/providers/workorder/.gitignore create mode 100644 frontend/providers/workorder/.prettierignore create mode 100644 frontend/providers/workorder/.prettierrc.js create mode 100644 frontend/providers/workorder/README.md create mode 100644 frontend/providers/workorder/deploy/Kubefile create mode 100644 frontend/providers/workorder/deploy/manifests/appcr.yaml.tmpl create mode 100644 frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl create mode 100644 frontend/providers/workorder/deploy/manifests/ingress.yaml.tmpl create mode 100644 frontend/providers/workorder/i18nrc.json create mode 100644 frontend/providers/workorder/next-i18next.config.js create mode 100644 frontend/providers/workorder/next.config.js create mode 100644 frontend/providers/workorder/package.json create mode 100644 frontend/providers/workorder/public/favicon.ico create mode 100644 frontend/providers/workorder/public/icons/download.svg create mode 100644 frontend/providers/workorder/public/icons/sealos.svg create mode 100644 frontend/providers/workorder/public/icons/token.svg create mode 100644 frontend/providers/workorder/public/icons/wechat.svg create mode 100644 frontend/providers/workorder/public/locales/en/common.json create mode 100644 frontend/providers/workorder/public/locales/zh/common.json create mode 100644 frontend/providers/workorder/public/logo.svg create mode 100644 frontend/providers/workorder/src/api/platform.ts create mode 100644 frontend/providers/workorder/src/api/user.ts create mode 100644 frontend/providers/workorder/src/api/workorder.ts create mode 100644 frontend/providers/workorder/src/components/AppStatusTag/index.tsx create mode 100644 frontend/providers/workorder/src/components/FileSelect/index.tsx create mode 100644 frontend/providers/workorder/src/components/Icon/icons/analyze.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/change.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/close.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/connection.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/continue.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/copy.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/delete.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/detail.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/download.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/formInfo.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/info.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/link.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/log.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/logo.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/more.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/noEvents.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/pause.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/plus.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/podList.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/read.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/restart.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/restore.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/robot.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/settings.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/start.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/terminal.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/unread.svg create mode 100644 frontend/providers/workorder/src/components/Icon/icons/warning.svg create mode 100644 frontend/providers/workorder/src/components/Icon/index.tsx create mode 100644 frontend/providers/workorder/src/components/Markdown/CodeLight.tsx create mode 100644 frontend/providers/workorder/src/components/Markdown/img/Image.tsx create mode 100644 frontend/providers/workorder/src/components/Markdown/index.module.scss create mode 100644 frontend/providers/workorder/src/components/Markdown/index.tsx create mode 100644 frontend/providers/workorder/src/components/Menu/index.tsx create mode 100644 frontend/providers/workorder/src/components/MonitorTabs/index.tsx create mode 100644 frontend/providers/workorder/src/components/MyTooltip/index.tsx create mode 100644 frontend/providers/workorder/src/components/Pagination/index.tsx create mode 100644 frontend/providers/workorder/src/components/RangeDate/index.tsx create mode 100644 frontend/providers/workorder/src/components/RangeDate/style.module.scss create mode 100644 frontend/providers/workorder/src/components/Select/index.tsx create mode 100644 frontend/providers/workorder/src/components/SelectDateRange/index.tsx create mode 100644 frontend/providers/workorder/src/components/Table/index.tsx create mode 100644 frontend/providers/workorder/src/components/Tabs/index.tsx create mode 100644 frontend/providers/workorder/src/components/Tip/index.tsx create mode 100644 frontend/providers/workorder/src/constants/theme.ts create mode 100644 frontend/providers/workorder/src/constants/workorder.ts create mode 100644 frontend/providers/workorder/src/hooks/useConfirm.tsx create mode 100644 frontend/providers/workorder/src/hooks/useLoading.tsx create mode 100644 frontend/providers/workorder/src/hooks/useScreen.ts create mode 100644 frontend/providers/workorder/src/hooks/useSelectFile.tsx create mode 100644 frontend/providers/workorder/src/hooks/useStore.ts create mode 100644 frontend/providers/workorder/src/hooks/useToast.ts create mode 100644 frontend/providers/workorder/src/pages/404.tsx create mode 100644 frontend/providers/workorder/src/pages/_app.tsx create mode 100644 frontend/providers/workorder/src/pages/_document.tsx create mode 100644 frontend/providers/workorder/src/pages/api/ai/fastgpt.ts create mode 100644 frontend/providers/workorder/src/pages/api/auth/desktop.ts create mode 100644 frontend/providers/workorder/src/pages/api/auth/findById.ts create mode 100644 frontend/providers/workorder/src/pages/api/minio/delete.ts create mode 100644 frontend/providers/workorder/src/pages/api/minio/getUrl.ts create mode 100644 frontend/providers/workorder/src/pages/api/minio/upload.ts create mode 100644 frontend/providers/workorder/src/pages/api/platform/feishu.ts create mode 100644 frontend/providers/workorder/src/pages/api/platform/getEnv.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/create.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/delete.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/getByID.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/listByUser.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/update.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts create mode 100644 frontend/providers/workorder/src/pages/index.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/create/index.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/detail/components/AppBaseInfo.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/detail/components/Header.tsx create mode 100644 frontend/providers/workorder/src/pages/workorder/detail/index.tsx create mode 100644 frontend/providers/workorder/src/pages/workorders/components/List.tsx create mode 100644 frontend/providers/workorder/src/pages/workorders/index.tsx create mode 100644 frontend/providers/workorder/src/services/backend/auth.ts create mode 100644 frontend/providers/workorder/src/services/backend/kubernetes.ts create mode 100644 frontend/providers/workorder/src/services/backend/response.ts create mode 100644 frontend/providers/workorder/src/services/db/mongodb.ts create mode 100644 frontend/providers/workorder/src/services/db/region.ts create mode 100644 frontend/providers/workorder/src/services/db/user.ts create mode 100644 frontend/providers/workorder/src/services/db/workorder.ts create mode 100644 frontend/providers/workorder/src/services/error.ts create mode 100644 frontend/providers/workorder/src/services/kubernet.ts create mode 100644 frontend/providers/workorder/src/services/request.ts create mode 100644 frontend/providers/workorder/src/store/env.ts create mode 100644 frontend/providers/workorder/src/store/global.ts create mode 100644 frontend/providers/workorder/src/store/session.ts create mode 100644 frontend/providers/workorder/src/styles/reset.scss create mode 100644 frontend/providers/workorder/src/types/index.ts create mode 100644 frontend/providers/workorder/src/types/region.ts create mode 100644 frontend/providers/workorder/src/types/user.ts create mode 100644 frontend/providers/workorder/src/types/workorder.ts create mode 100644 frontend/providers/workorder/src/utils/cookieUtils.ts create mode 100644 frontend/providers/workorder/src/utils/crypto.ts create mode 100644 frontend/providers/workorder/src/utils/file.ts create mode 100644 frontend/providers/workorder/src/utils/i18n.ts create mode 100644 frontend/providers/workorder/src/utils/tools.ts create mode 100644 frontend/providers/workorder/tsconfig.json diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 26a93a658..f2c122613 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -66,6 +66,7 @@ jobs: providers/costcenter, providers/objectstorage, providers/kubepanel, + providers/workorder, desktop, ] steps: @@ -163,6 +164,7 @@ jobs: providers/costcenter, providers/objectstorage, providers/kubepanel, + providers/workorder, desktop, ] steps: diff --git a/frontend/Makefile b/frontend/Makefile index c30e77b14..abaa889d9 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -25,7 +25,8 @@ buildTargets := \ providers/cronjob \ providers/license \ providers/kubepanel \ - providers/objectstorage + providers/objectstorage \ + providers/workorder buildTargets-all := $(addprefix image-build-,$(buildTargets)) pushTargets-all := $(addprefix image-push-,$(buildTargets)) diff --git a/frontend/package.json b/frontend/package.json index 776e5ae21..1c58f207c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "dev-template": "pnpm -r --filter ./providers/template run dev", "dev-cronjob": "pnpm -r --filter ./providers/cronjob run dev", "dev-license": "pnpm -r --filter ./providers/license run dev", + "dev-invite": "pnpm -r --filter ./providers/invite run dev", + "dev-workorder": "pnpm -r --filter ./providers/workorder run dev", "gen:theme-typings": "pnpm chakra-cli tokens packages/ui/src/theme/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts", "postinstall": "pnpm run gen:theme-typings" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index efc91326f..656f6eea1 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 2.2.0(@chakra-ui/react@2.8.2)(@emotion/react@11.11.1)(next@13.5.6)(react@18.2.0) '@chakra-ui/react': specifier: ^2.8.1 - version: 2.8.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) + version: 2.8.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/system': specifier: ^2.6.1 version: 2.6.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) @@ -28,7 +28,7 @@ importers: version: 11.11.1(@types/react@18.2.37)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.1)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) framer-motion: specifier: ^10.16.4 version: 10.16.5(react-dom@18.2.0)(react@18.2.0) @@ -2180,6 +2180,190 @@ importers: specifier: ^1.68.0 version: 1.69.5 + providers/workorder: + dependencies: + '@chakra-ui/anatomy': + specifier: ^2.2.1 + version: 2.2.2 + '@chakra-ui/icons': + specifier: ^2.1.1 + version: 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/next-js': + specifier: ^2.1.5 + version: 2.2.0(@chakra-ui/react@2.8.2)(@emotion/react@11.11.1)(next@13.1.6)(react@18.2.0) + '@chakra-ui/react': + specifier: ^2.8.1 + version: 2.8.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': + specifier: ^2.6.1 + version: 2.6.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@emotion/react': + specifier: ^11.11.1 + version: 11.11.1(@types/react@18.2.37)(react@18.2.0) + '@emotion/styled': + specifier: ^11.11.0 + version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0) + '@fortaine/fetch-event-source': + specifier: ^3.0.6 + version: 3.0.6 + '@kubernetes/client-node': + specifier: ^0.18.1 + version: 0.18.1 + '@next/font': + specifier: 13.1.6 + version: 13.1.6 + '@tanstack/react-query': + specifier: ^4.35.3 + version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@types/multer': + specifier: ^1.4.10 + version: 1.4.11 + ansi_up: + specifier: ^5.2.1 + version: 5.2.1 + axios: + specifier: ^1.5.1 + version: 1.6.2 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 + echarts: + specifier: ^5.4.3 + version: 5.4.3 + framer-motion: + specifier: ^10.16.4 + version: 10.16.5(react-dom@18.2.0)(react@18.2.0) + i18next: + specifier: ^22.5.1 + version: 22.5.1 + immer: + specifier: ^9.0.21 + version: 9.0.21 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + jszip: + specifier: ^3.10.1 + version: 3.10.1 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + minio: + specifier: ^7.1.3 + version: 7.1.3 + mongodb: + specifier: ^5.9.0 + version: 5.9.1 + multer: + specifier: 1.4.5-lts.1 + version: 1.4.5-lts.1 + nanoid: + specifier: ^4.0.2 + version: 4.0.2 + next: + specifier: 13.1.6 + version: 13.1.6(@babel/core@7.23.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + next-i18next: + specifier: ^13.3.0 + version: 13.3.0(i18next@22.5.1)(next@13.1.6)(react-i18next@12.3.1)(react@18.2.0) + nprogress: + specifier: ^0.2.0 + version: 0.2.0 + prettier: + specifier: ^2.8.8 + version: 2.8.8 + react: + specifier: 18.2.0 + version: 18.2.0 + react-day-picker: + specifier: ^8.8.2 + version: 8.9.1(date-fns@2.30.0)(react@18.2.0) + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.46.2 + version: 7.48.2(react@18.2.0) + react-i18next: + specifier: ^12.3.1 + version: 12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) + react-markdown: + specifier: ^8.0.7 + version: 8.0.7(@types/react@18.2.37)(react@18.2.0) + react-syntax-highlighter: + specifier: ^15.5.0 + version: 15.5.0(react@18.2.0) + remark-breaks: + specifier: ^4.0.0 + version: 4.0.0 + remark-gfm: + specifier: ^3.0.1 + version: 3.0.1 + sass: + specifier: ^1.68.0 + version: 1.69.5 + sealos-desktop-sdk: + specifier: workspace:* + version: link:../../packages/client-sdk + typescript: + specifier: ^5.2.2 + version: 5.2.2 + zustand: + specifier: ^4.4.1 + version: 4.4.7(@types/react@18.2.37)(immer@9.0.21)(react@18.2.0) + devDependencies: + '@svgr/webpack': + specifier: ^6.5.1 + version: 6.5.1 + '@types/js-cookie': + specifier: ^3.0.4 + version: 3.0.6 + '@types/js-yaml': + specifier: ^4.0.6 + version: 4.0.9 + '@types/jsonwebtoken': + specifier: ^9.0.3 + version: 9.0.5 + '@types/lodash': + specifier: ^4.14.199 + version: 4.14.202 + '@types/node': + specifier: 18.13.0 + version: 18.13.0 + '@types/nprogress': + specifier: ^0.2.1 + version: 0.2.3 + '@types/react': + specifier: 18.2.37 + version: 18.2.37 + '@types/react-dom': + specifier: 18.0.10 + version: 18.0.10 + '@types/react-syntax-highlighter': + specifier: ^15.5.7 + version: 15.5.10 + eslint: + specifier: 8.33.0 + version: 8.33.0 + eslint-config-next: + specifier: 13.1.6 + version: 13.1.6(eslint@8.33.0)(typescript@5.2.2) + svg-inline-loader: + specifier: ^0.8.2 + version: 0.8.2 + webpack-bundle-analyzer: + specifier: ^4.9.1 + version: 4.10.1 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -3857,7 +4041,7 @@ packages: resolution: {integrity: sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.4 + '@babel/types': 7.23.5 '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.20 jsesc: 2.5.2 @@ -5897,77 +6081,6 @@ packages: - '@types/react' dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.16.5)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.1)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.37)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.16.5)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.37)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.16.5)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.16.5)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.16.5)(react@18.2.0) - '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - framer-motion: 10.16.5(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} peerDependencies: @@ -6068,7 +6181,7 @@ packages: resolution: {integrity: sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==} dependencies: '@chakra-ui/shared-utils': 2.0.5 - csstype: 3.1.2 + csstype: 3.1.3 lodash.mergewith: 4.6.2 dev: false @@ -6452,27 +6565,6 @@ packages: dev: false /@emotion/styled@11.11.0(@emotion/react@11.11.1)(@types/react@18.2.37)(react@18.2.0): - resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} - peerDependencies: - '@emotion/react': ^11.0.0-rc.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.4 - '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.1 - '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) - '@emotion/serialize': 1.1.2 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@emotion/utils': 1.2.1 - '@types/react': 18.2.37 - react: 18.2.0 - dev: false - - /@emotion/styled@11.11.0(@emotion/react@11.11.1)(react@18.2.0): resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 @@ -6489,6 +6581,7 @@ packages: '@emotion/serialize': 1.1.2 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 + '@types/react': 18.2.37 react: 18.2.0 dev: false @@ -6826,6 +6919,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@fortaine/fetch-event-source@3.0.6: + resolution: {integrity: sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==} + engines: {node: '>=16.15'} + dev: false + /@humanwhocodes/config-array@0.11.13: resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -9181,7 +9279,7 @@ packages: resolution: {integrity: sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==} engines: {node: '>=10'} dependencies: - '@babel/types': 7.23.4 + '@babel/types': 7.23.5 entities: 4.5.0 dev: true @@ -9667,6 +9765,12 @@ packages: '@types/unist': 2.0.10 dev: false + /@types/mdast@4.0.3: + resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} + dependencies: + '@types/unist': 2.0.10 + dev: false + /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -9828,6 +9932,10 @@ packages: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: false + /@types/unist@3.0.2: + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + dev: false + /@types/uuid@9.0.7: resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} dev: true @@ -10987,7 +11095,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001566 + caniuse-lite: 1.0.30001594 electron-to-chromium: 1.4.603 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) @@ -11118,6 +11226,7 @@ packages: /caniuse-lite@1.0.30001566: resolution: {integrity: sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==} + dev: false /caniuse-lite@1.0.30001594: resolution: {integrity: sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g==} @@ -12095,6 +12204,12 @@ packages: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + dev: false + /dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} dependencies: @@ -16575,6 +16690,15 @@ packages: unist-util-visit-parents: 5.1.3 dev: false + /mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + dependencies: + '@types/mdast': 4.0.3 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + /mdast-util-from-markdown@1.3.1: resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} dependencies: @@ -16650,6 +16774,13 @@ packages: - supports-color dev: false + /mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + dependencies: + '@types/mdast': 4.0.3 + mdast-util-find-and-replace: 3.0.1 + dev: false + /mdast-util-phrasing@3.0.1: resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} dependencies: @@ -19516,6 +19647,14 @@ packages: unist-util-visit: 4.1.2 dev: false + /remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + dependencies: + '@types/mdast': 4.0.3 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.4 + dev: false + /remark-gfm@3.0.1: resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} dependencies: @@ -21214,6 +21353,18 @@ packages: vfile: 5.3.7 dev: false + /unified@11.0.4: + resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} + dependencies: + '@types/unist': 3.0.2 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 6.0.1 + dev: false + /union-value@1.0.1: resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} engines: {node: '>=0.10.0'} @@ -21241,6 +21392,12 @@ packages: '@types/unist': 2.0.10 dev: false + /unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /unist-util-position@4.0.4: resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} dependencies: @@ -21253,6 +21410,12 @@ packages: '@types/unist': 2.0.10 dev: false + /unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /unist-util-visit-parents@5.1.3: resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} dependencies: @@ -21260,6 +21423,13 @@ packages: unist-util-is: 5.2.1 dev: false + /unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + dev: false + /unist-util-visit@4.1.2: resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} dependencies: @@ -21471,6 +21641,13 @@ packages: unist-util-stringify-position: 3.0.3 dev: false + /vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + dev: false + /vfile@5.3.7: resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} dependencies: @@ -21480,6 +21657,14 @@ packages: vfile-message: 3.1.4 dev: false + /vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + dev: false + /viewport-mercator-project@6.2.3: resolution: {integrity: sha512-QQb0/qCLlP4DdfbHHSWVYXpghB2wkLIiiZQnoelOB59mXKQSyZVxjreq1S+gaBJFpcGkWEcyVtre0+2y2DTl/Q==} dependencies: diff --git a/frontend/providers/workorder/.env.template b/frontend/providers/workorder/.env.template new file mode 100644 index 000000000..ec888eb5b --- /dev/null +++ b/frontend/providers/workorder/.env.template @@ -0,0 +1,15 @@ +NEXT_PUBLIC_MOCK_USER= +SEALOS_DOMAIN="cloud.sealos.io" + +# mongodb +MONGODB_URI= +# minio +MINIO_URL= +MINIO_BUCKET_NAME= +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_PORT= +# admin +ADMIN_FEISHU_URL= +JWT_SECRET_DESKTOP_TO_APP= +JWT_SECRET_SELF= \ No newline at end of file diff --git a/frontend/providers/workorder/.eslintrc.json b/frontend/providers/workorder/.eslintrc.json new file mode 100644 index 000000000..578210f47 --- /dev/null +++ b/frontend/providers/workorder/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "next/core-web-vitals", + "rules": { + "react-hooks/rules-of-hooks": 0 + } +} diff --git a/frontend/providers/workorder/.gitignore b/frontend/providers/workorder/.gitignore new file mode 100644 index 000000000..52364be8e --- /dev/null +++ b/frontend/providers/workorder/.gitignore @@ -0,0 +1,41 @@ +# 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 +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 +/public/trainData/ +/.vscode/ +platform.json +.yalc/ +yalc.lock diff --git a/frontend/providers/workorder/.prettierignore b/frontend/providers/workorder/.prettierignore new file mode 100644 index 000000000..7aef7f6b0 --- /dev/null +++ b/frontend/providers/workorder/.prettierignore @@ -0,0 +1,4 @@ +dist +.vscode +**/.DS_Store +node_modules diff --git a/frontend/providers/workorder/.prettierrc.js b/frontend/providers/workorder/.prettierrc.js new file mode 100644 index 000000000..cb1553cbe --- /dev/null +++ b/frontend/providers/workorder/.prettierrc.js @@ -0,0 +1,21 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: true, + quoteProps: 'as-needed', + jsxSingleQuote: false, + trailingComma: 'none', + bracketSpacing: true, + jsxBracketSameLine: false, + arrowParens: 'always', + rangeStart: 0, + rangeEnd: Infinity, + requirePragma: false, + insertPragma: false, + proseWrap: 'preserve', + htmlWhitespaceSensitivity: 'css', + endOfLine: 'lf' +}; + \ No newline at end of file diff --git a/frontend/providers/workorder/README.md b/frontend/providers/workorder/README.md new file mode 100644 index 000000000..adc6728f4 --- /dev/null +++ b/frontend/providers/workorder/README.md @@ -0,0 +1,14 @@ +# Sealos WorkOrder + +## Dependencies + +- Minio service +- MongoDB service + +## Feishu Integration + +Sealos WorkOrder can be integrated with Feishu to enable message notifications. + +### Environment Variable Configuration + +Create a `.env` file and refer to `.env.template` for the content. diff --git a/frontend/providers/workorder/deploy/Kubefile b/frontend/providers/workorder/deploy/Kubefile new file mode 100644 index 000000000..56887ad60 --- /dev/null +++ b/frontend/providers/workorder/deploy/Kubefile @@ -0,0 +1,20 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY manifests manifests + +ENV cloudDomain="127.0.0.1.nip.io" +ENV cloudPort="" +ENV certSecretName="wildcard-cert" +ENV mongoUrl="" +ENV minioUrl="" +ENV minioAccessKey="" +ENV minioSecretKey="" +ENV minioPort="" +ENV minioBucketName="" +ENV adminFeishuUrl="" +ENV adminFeishuCallBackUrl="" + +CMD ["kubectl apply -f manifests"] diff --git a/frontend/providers/workorder/deploy/manifests/appcr.yaml.tmpl b/frontend/providers/workorder/deploy/manifests/appcr.yaml.tmpl new file mode 100644 index 000000000..28085c726 --- /dev/null +++ b/frontend/providers/workorder/deploy/manifests/appcr.yaml.tmpl @@ -0,0 +1,21 @@ +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: workorder + namespace: app-system +spec: + data: + desc: workorder + url: "https://workorder.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}" + icon: "https://workorder.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}/logo.svg" + menuData: + helpDropDown: false + nameColor: text-black + name: workorder + type: iframe + displayType: normal + i18n: + zh: + name: 工单 + zh-Hans: + name: 工单 diff --git a/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl new file mode 100644 index 000000000..541b20fd3 --- /dev/null +++ b/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl @@ -0,0 +1,90 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app: workorder-frontend + name: workorder-frontend +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: workorder-frontend-config + namespace: workorder-frontend +data: + config.yaml: |- + addr: :3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workorder-frontend + namespace: workorder-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: workorder-frontend + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% + template: + metadata: + labels: + app: workorder-frontend + spec: + containers: + - name: workorder-frontend + env: + - name: SEALOS_DOMAIN + value: {{ .cloudDomain }} + - name: MONGODB_URI + - name: MINIO_URL + - name: MINIO_BUCKET_NAME + - name: MINIO_ACCESS_KEY + - name: MINIO_SECRET_KEY + - name: MINIO_PORT + - name: ADMIN_FEISHU_URL + - name: ADMIN_FEISHU_CALLBACK_URL + securityContext: + runAsNonRoot: true + runAsUser: 1001 + allowPrivilegeEscalation: false + capabilities: + drop: + - 'ALL' + resources: + limits: + cpu: 1000m + memory: 1024Mi + requests: + cpu: 10m + memory: 128Mi + # do not modify this image, it is used for CI/CD + image: ghcr.io/labring/sealos-workorder-frontend:latest + imagePullPolicy: Always + volumeMounts: + - name: workorder-frontend-volume + mountPath: /config.yaml + subPath: config.yaml + volumes: + - name: workorder-frontend-volume + configMap: + name: workorder-frontend-config +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: workorder-frontend + name: workorder-frontend + namespace: workorder-frontend +spec: + ports: + - name: http + port: 3000 + protocol: TCP + targetPort: 3000 + selector: + app: workorder-frontend diff --git a/frontend/providers/workorder/deploy/manifests/ingress.yaml.tmpl b/frontend/providers/workorder/deploy/manifests/ingress.yaml.tmpl new file mode 100644 index 000000000..41faa99df --- /dev/null +++ b/frontend/providers/workorder/deploy/manifests/ingress.yaml.tmpl @@ -0,0 +1,51 @@ +# Copyright © 2023 sealos. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, DELETE, PATCH, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-origin: "https://{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}, https://*.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}" + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + nginx.ingress.kubernetes.io/cors-max-age: "600" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + more_clear_headers "X-Frame-Options:"; + more_set_headers "Content-Security-Policy: default-src * blob: data: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}; img-src * data: blob: resource: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}; connect-src * wss: blob: resource:; style-src 'self' 'unsafe-inline' blob: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} resource:; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} resource: *.baidu.com *.bdstatic.com https://js.stripe.com; frame-src 'self' *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} mailto: tel: weixin: mtt: *.baidu.com https://js.stripe.com; frame-ancestors 'self' https://{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} https://*.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}"; + more_set_headers "X-Xss-Protection: 1; mode=block"; + higress.io/response-header-control-remove: X-Frame-Options + higress.io/response-header-control-update: | + Content-Security-Policy "default-src * blob: data: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}; img-src * data: blob: resource: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}; connect-src * wss: blob: resource:; style-src 'self' 'unsafe-inline' blob: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} resource:; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} resource: *.baidu.com *.bdstatic.com https://js.stripe.com; frame-src 'self' *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} mailto: tel: weixin: mtt: *.baidu.com https://js.stripe.com; frame-ancestors 'self' https://{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} https://*.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}" + X-Xss-Protection "1; mode=block" + name: workorder-frontend + namespace: workorder-frontend +spec: + rules: + - host: workorder.{{ .cloudDomain }} + http: + paths: + - pathType: Prefix + path: / + backend: + service: + name: workorder-frontend + port: + number: 3000 + tls: + - hosts: + - workorder.{{ .cloudDomain }} + secretName: {{ .certSecretName }} diff --git a/frontend/providers/workorder/i18nrc.json b/frontend/providers/workorder/i18nrc.json new file mode 100644 index 000000000..c39846f7f --- /dev/null +++ b/frontend/providers/workorder/i18nrc.json @@ -0,0 +1,8 @@ +{ + "entry": "./public/locales/zh", + "entryLocale": "zh", + "output": "./public/locales", + "outputLocales": [ + "en" + ] +} \ No newline at end of file diff --git a/frontend/providers/workorder/next-i18next.config.js b/frontend/providers/workorder/next-i18next.config.js new file mode 100644 index 000000000..1a032fe9e --- /dev/null +++ b/frontend/providers/workorder/next-i18next.config.js @@ -0,0 +1,12 @@ +//next-i18next.config.js +/** + * @type {import('next-i18next').UserConfig} + */ + +module.exports = { + i18n: { + defaultLocale: 'zh', + locales: ['en', 'zh'], + localeDetection: false + } +} diff --git a/frontend/providers/workorder/next.config.js b/frontend/providers/workorder/next.config.js new file mode 100644 index 000000000..a7dce48a9 --- /dev/null +++ b/frontend/providers/workorder/next.config.js @@ -0,0 +1,28 @@ +/** @type {import('next').NextConfig} */ +const { i18n } = require('./next-i18next.config') +const path = require('path') + +const nextConfig = { + i18n, + output: 'standalone', + reactStrictMode: false, + compress: true, + webpack: (config, { isServer }) => { + config.module.rules = config.module.rules.concat([ + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + use: ['@svgr/webpack'] + } + ]) + config.plugins = [...config.plugins] + return config + }, + transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk', '@sealos/driver'], + experimental: { + // this includes files from the monorepo base two directories up + outputFileTracingRoot: path.join(__dirname, '../../') + } +} + +module.exports = nextConfig diff --git a/frontend/providers/workorder/package.json b/frontend/providers/workorder/package.json new file mode 100644 index 000000000..42fd27987 --- /dev/null +++ b/frontend/providers/workorder/package.json @@ -0,0 +1,75 @@ +{ + "name": "workorder", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@chakra-ui/anatomy": "^2.2.1", + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/next-js": "^2.1.5", + "@chakra-ui/react": "^2.8.1", + "@chakra-ui/system": "^2.6.1", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@fortaine/fetch-event-source": "^3.0.6", + "@kubernetes/client-node": "^0.18.1", + "@next/font": "13.1.6", + "@tanstack/react-query": "^4.35.3", + "@types/multer": "^1.4.10", + "ansi_up": "^5.2.1", + "axios": "^1.5.1", + "date-fns": "^2.30.0", + "dayjs": "^1.11.10", + "echarts": "^5.4.3", + "framer-motion": "^10.16.4", + "i18next": "^22.5.1", + "immer": "^9.0.21", + "js-cookie": "^3.0.5", + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", + "lodash": "^4.17.21", + "minio": "^7.1.3", + "mongodb": "^5.9.0", + "multer": "1.4.5-lts.1", + "nanoid": "^4.0.2", + "next": "13.1.6", + "next-i18next": "^13.3.0", + "nprogress": "^0.2.0", + "prettier": "^2.8.8", + "react": "18.2.0", + "react-day-picker": "^8.8.2", + "react-dom": "18.2.0", + "react-hook-form": "^7.46.2", + "react-i18next": "^12.3.1", + "react-markdown": "^8.0.7", + "react-syntax-highlighter": "^15.5.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^3.0.1", + "sass": "^1.68.0", + "sealos-desktop-sdk": "workspace:*", + "typescript": "^5.2.2", + "zustand": "^4.4.1" + }, + "devDependencies": { + "@svgr/webpack": "^6.5.1", + "@types/js-cookie": "^3.0.4", + "@types/js-yaml": "^4.0.6", + "@types/jsonwebtoken": "^9.0.3", + "@types/lodash": "^4.14.199", + "@types/node": "18.13.0", + "@types/nprogress": "^0.2.1", + "@types/react": "18.2.37", + "@types/react-dom": "18.0.10", + "@types/react-syntax-highlighter": "^15.5.7", + "eslint": "8.33.0", + "eslint-config-next": "13.1.6", + "svg-inline-loader": "^0.8.2", + "webpack-bundle-analyzer": "^4.9.1" + } +} \ No newline at end of file diff --git a/frontend/providers/workorder/public/favicon.ico b/frontend/providers/workorder/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c594435402b65b109de7e710f99ea3726a5a9f42 GIT binary patch literal 4286 zcmcJSO>7)z8OI-!II$_Xg+mh3O=u5Da445bi;4v5r9#SyQd)UGXLojIXLh~*E-42i z1yx9uI6?&ymx>q#xkcfl@&P2;$nkpZ@y6cuuD_6zw2oAjwp=JxMH3Id_kCy9b{c3e zZ1nd#-i`J*|Nrwm?>iRh;on0KiTWwOeL&=dhpUrQxrf$y$yA9Z+j zD=mwB&r(jU>Rz(4nB(_+27X4C3pw}}tEIUsrMzAhs-8~sU3^w5g=CTWL_!pQA%WCd zc~bFv7gEX3!vFkyDmhTkCI_Mw=DAO7?xL zrTj79%JnoWMcE#=WiD3{zvS-o$y()IQ|x52ua-)jn@=WwiT((G7XPyg>68}!XJO8A z4_ZsP%q$!&-_Fi1+tw_;XSoNhYfkP}{QsK!$gVi~`&UajX?q#j85!(mp!_RcO(Z^3 zOC-JvcMAndumf@r;0LgSdR8eNzz<*><+Fj0UppvhnnAky%%L3`R?X!G7s6>d(bQw-h}^XbEqiILP45NLG#I7Wv>*2 zT{rdKXH)OLQ}18U-{+UC%y~bD?CkkvE$e5nZz*oWIZ$)*rsiaBE8mJ8v?|426i1?F zL9Obg<~mf2by4#V);pX2!;+OgX=a`oXWrrK`EJh&=92AO{c<{96%wAka$u+N-XF6O@7aPo7i*LANQ$X)x^ z-ZeW|8*+A0yglS-c8gWL;6;jYABNwZGu`L6;orn}i+gBN^Cp=$@vL-%8UJ^e?;h&6 z$hTsL{9?R4T#^NGiMDo=l^}q)U0*c)Y&ThNv z#G6$|HpzZH%0G*LqZpZM{$2aa`|$rtzfI$N8um2oX?$yu`!q9VD<4#<5M#n(ql(j1}&As?R5XG$@y;hViJ%AA_x-Fl=^ zEc7iGvi)%S@z~FN^)DCHwbS1$+x@MwgY3SwO7Z^IP;ru)e-Qds?4YChZhT|ZmDNN1 zj6P4{!p|&5`?}_wUo<7iow?W18j4qsQo4Cw{XVI{!$2gP46#i#sj?TexuhS-vQiU zebn9E7<1#~AX}4V*_`k+--jPc^Y3Qk{Ug{r_+F;he9lrIwolD{eEZbGr{8?oKAx3Y zu>YYvhrShC^Vdf`?Y}eT$vRRmcwIi%i(P&=W3cPjin(vWKMDJkmRmRlTMPCnCHSXc zpJY8t?$5*iF!XKsW8Ut@xEo&|E#sdVoG|(Od5e2MIV-05cb%p1UuA!ZUCS56FW9oo zxzTVPS@DwW<2>(qWvTH@(;j7}#yzQJPe^;jwPC;C<&Sy62K>!2FWwoi$Tt6Xi$3i7 zR~h89F?zioooTAxO89<{++QN|AJO|$^xG7@hKf^oonpqO=yNS{pQ64mz+FarCU?ya zHpa{Nuf$dETN73M4{1I;3H2arje2XYVe<@gu;0vcM0B2o{bu%?A&RtU*Go~JwJtK={?y|uOFJTU|sjSVZ&4GU~{5!b8E8l$R_#Un5xR( z&WHZhv$D|x>}@Y~ed{xgGgNU8u*0)_k6wERy@l`Fy3f@fW9Dq%H+45`&0m0jM)_}7 zN^w$faURy)A>2vc8ae&=2=eXL}uTysy_EWHZYOdLa?*-IY z^;^W=+nT7{f?eL8bdPOMdb0g&S#Dey(|o_cvjqNO%Bo`@V?Tc!_D}Kr8+z?!=Ik;v zewluorQhB{YJa)X9(KOmuDZuzUsT-AsQXX)?a$QwGIQ{IYWzFAUfP@}zex5^ZA^Nf zTdNL9!yb`(a-z#`I60mL?XKn$YPRfN*nMiBH!Ak=%ya_Jzk;pKly~v{^UkpQJnZYH z-tLvosP{1Ze4R4aa*9s^v%o=~U z+0x`W0lt1-f8#vQOhe8&*gN6Aeh2nPCUf__^)a_h#z)D$+uy3z@MUwdA|L+bIl1IM zA-4wJmSE=T2(8x@odG!vdmrPIzi@8L8fVsO#p!2F!M}?4cVNFu<}EbY87+OWJ>m#w z;XV07-*`-FeD>Jc*dg{O{c`cigzWyHBAs#0ZO*Nh>l`KfhdU#sC*YqW@2_o4czvw$ zO}=dBha>ctnXOOy?oadiYq^}P!tG?pf3zrTWzJ5X9Xl0Q6jMDEm4p!6R!%hY86 literal 0 HcmV?d00001 diff --git a/frontend/providers/workorder/public/icons/download.svg b/frontend/providers/workorder/public/icons/download.svg new file mode 100644 index 000000000..8aed8eea5 --- /dev/null +++ b/frontend/providers/workorder/public/icons/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/providers/workorder/public/icons/sealos.svg b/frontend/providers/workorder/public/icons/sealos.svg new file mode 100644 index 000000000..70e732656 --- /dev/null +++ b/frontend/providers/workorder/public/icons/sealos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/public/icons/token.svg b/frontend/providers/workorder/public/icons/token.svg new file mode 100644 index 000000000..b0dd76f35 --- /dev/null +++ b/frontend/providers/workorder/public/icons/token.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/providers/workorder/public/icons/wechat.svg b/frontend/providers/workorder/public/icons/wechat.svg new file mode 100644 index 000000000..b713640cb --- /dev/null +++ b/frontend/providers/workorder/public/icons/wechat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/providers/workorder/public/locales/en/common.json b/frontend/providers/workorder/public/locales/en/common.json new file mode 100644 index 000000000..1811debd7 --- /dev/null +++ b/frontend/providers/workorder/public/locales/en/common.json @@ -0,0 +1,64 @@ +{ + "All": "All", + "Are you sure you want to submit this work order": "Are you sure you want to submit this work order?", + "Cancel": "Cancel", + "Close": "Close", + "Completed": "Completed", + "Confirm": "Confirm", + "Create Time": "Create Time ", + "Creating": "Creating", + "Delete": "Delete", + "Deleted": "Deleted", + "Description": "Description", + "Details": "Details", + "Failed": "Failed", + "File upload failed": "File upload failed", + "functionserver": "Function Server", + "Name": "Name", + "New Order": "New Order", + "None": "None", + "Notification SwitchToManual Tips": "The request was sent successfully, please wait patiently.", + "objectstorage": "Object Storage", + "Operation": "Operation", + "Order Conversation": "Order Conversation", + "Order Detail": "Order Detail", + "Order Empty": "Order Empty", + "Order List": "Order List", + "Order Name": "Order Name", + "Page": "Page", + "Pending": "Pending", + "Problem Description": "Problem Description", + "Processing": "Processing", + "Prompt": "Prompt", + "Question Type": "Question Type", + "Request exceeds limit": "Request exceeds limit", + "Send": "Send", + "Status": "Status", + "Submit Error": "Submit Error", + "Submit Order": "Submit Order", + "Switch to manual": "Switch to manual", + "Total": "Total", + "Type": "Type", + "Upload File": "Upload File", + "appendix": "appendix", + "applaunchpad": "App Launchpad", + "cloudserver": "Cloud Server", + "costcenter": "CostCenter", + "cronjob": "CronJob", + "dbprovider": "Database", + "describe your problem": "describe your problem", + "file": { + "Upload Success": "File uploaded successfully", + "Uploading": "Uploading file, Progress: {{percent}}%" + }, + "network anomaly": "Network anomaly", + "please wait for the end": "Chatting in progress... Please wait for it to finish", + "template": "App Store", + "user info": "User Info", + "send": "Send", + "newline": "newline", + "Copy": "Copy", + "api is error": "api is error", + "region": "region", + "userId": "userId" +} diff --git a/frontend/providers/workorder/public/locales/zh/common.json b/frontend/providers/workorder/public/locales/zh/common.json new file mode 100644 index 000000000..f59b5f71c --- /dev/null +++ b/frontend/providers/workorder/public/locales/zh/common.json @@ -0,0 +1,64 @@ +{ + "file": { + "Uploading": "正在上传文件,进度: {{percent}}%", + "Upload Success": "文件上传成功" + }, + "All": "全部", + "Total": "总数", + "Page": "页", + "Creating": "创建中", + "Failed": "有异常", + "Name": "名字", + "Cancel": "取消", + "Confirm": "确认", + "New Order": "新建工单", + "Submit Order": "提交工单", + "Problem Description": "问题描述", + "appendix": "附件", + "Description": "详细描述", + "Type": "类型", + "Order List": "工单列表", + "describe your problem": "描述您的问题", + "Are you sure you want to submit this work order": "确定提交此工单吗", + "Order Name": "工单号", + "Operation": "操作", + "Question Type": "问题类型", + "Status": "状态", + "Create Time": "创建时间", + "Details": "详情", + "Close": "关闭", + "Delete": "删除", + "Order Detail": "工单详情", + "Order Conversation": "工单对话", + "Send": "发送", + "Order Empty": "暂无工单", + "Upload File": "上传文件", + "None": "暂无", + "Processing": "进行中", + "Completed": "已完成", + "Prompt": "提示", + "File upload failed": "文件上传失败", + "Pending": "待处理", + "Submit Error": "提交错误", + "Deleted": "已删除", + "user info": "用户信息", + "Switch to manual": "转人工客服", + "network anomaly": "网络异常", + "Notification SwitchToManual Tips": "已转人工处理,请耐心等待。", + "please wait for the end": "正在聊天中...请等待结束", + "Request exceeds limit": "请求超出限制", + "applaunchpad": "应用管理", + "costcenter": "费用中心", + "template": "应用市场", + "dbprovider": "数据库", + "cronjob": "定时任务", + "cloudserver": "云主机", + "objectstorage": "对象存储", + "functionserver": "云开发", + "send": "发送", + "newline": "换行", + "Copy": "复制", + "api is error": "api is error", + "region": "可用区", + "userId": "用户ID" +} diff --git a/frontend/providers/workorder/public/logo.svg b/frontend/providers/workorder/public/logo.svg new file mode 100644 index 000000000..c72292f49 --- /dev/null +++ b/frontend/providers/workorder/public/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/api/platform.ts b/frontend/providers/workorder/src/api/platform.ts new file mode 100644 index 000000000..3a6aed711 --- /dev/null +++ b/frontend/providers/workorder/src/api/platform.ts @@ -0,0 +1,35 @@ +import { FeishuNotificationParams } from '@/pages/api/platform/feishu'; +import { SystemEnvResponse } from '@/pages/api/platform/getEnv'; +import { GET, POST } from '@/services/request'; +import { AxiosProgressEvent } from 'axios'; + +export const getSystemEnv = () => GET('/api/platform/getEnv'); + +export const uploadFile = ( + data: FormData, + onUploadProgress?: (progressEvent: AxiosProgressEvent) => void +) => { + return POST< + { + originalName: string; + fileName: string; + }[] + >('/api/minio/upload', data, { + headers: { + 'Content-Type': 'multipart/form-data' + }, + timeout: 3 * 60 * 1000, + onUploadProgress + }); +}; + +export const getFileUrl = ({ fileName }: { fileName: string }) => { + return GET('/api/minio/getUrl', { fileName }); +}; + +export const deleteFileByName = ({ fileName }: { fileName: string }) => { + return GET('/api/minio/delete', { fileName }); +}; + +export const FeishuNotification = (payload: FeishuNotificationParams) => + POST('/api/platform/feishu', payload); diff --git a/frontend/providers/workorder/src/api/user.ts b/frontend/providers/workorder/src/api/user.ts new file mode 100644 index 000000000..99137f9c4 --- /dev/null +++ b/frontend/providers/workorder/src/api/user.ts @@ -0,0 +1,9 @@ +import { GET, POST } from '@/services/request'; +import { RegionDB } from '@/types/region'; +import { AppSession, UserDB } from '@/types/user'; + +export const findUserById = ({ orderId }: { orderId: string }) => + GET<{ user: UserDB; regionInfo: RegionDB }>('/api/auth/findById', { orderId: orderId }); + +export const AuthByDesktopSession = (payload: { token: string }) => + POST('/api/auth/desktop', payload); diff --git a/frontend/providers/workorder/src/api/workorder.ts b/frontend/providers/workorder/src/api/workorder.ts new file mode 100644 index 000000000..038ab4565 --- /dev/null +++ b/frontend/providers/workorder/src/api/workorder.ts @@ -0,0 +1,30 @@ +import { CreateWorkOrderParams } from '@/pages/api/workorder/create'; +import { GET, POST } from '@/services/request'; +import { WorkOrderDB, WorkOrderStatus, WorkOrderType } from '@/types/workorder'; + +export const createWorkOrder = (payload: CreateWorkOrderParams) => + POST<{ orderId: string }>('/api/workorder/create', payload); + +export const getWorkOrderList = (payload: { + page: number; + pageSize: number; + orderType?: WorkOrderType; + orderStatus?: WorkOrderStatus; + startTime?: Date; + endTime?: Date; +}) => POST<{ totalCount: number; orders: WorkOrderDB[] }>('/api/workorder/listByUser', payload); + +export const getWorkOrderById = (payload: { orderId: string }) => + GET('/api/workorder/getByID', payload); + +export const delWorkOrderById = (payload: { orderId: string }) => + GET('/api/workorder/delete', payload); + +export const updateWorkOrderById = (payload: { updates: Partial; orderId: string }) => + POST('/api/workorder/update', payload); + +export const updateWorkOrderDialogById = (payload: { + orderId: string; + content: string; + isRobot?: boolean; +}) => POST('/api/workorder/updateDialog', payload); diff --git a/frontend/providers/workorder/src/components/AppStatusTag/index.tsx b/frontend/providers/workorder/src/components/AppStatusTag/index.tsx new file mode 100644 index 000000000..9bd941e5a --- /dev/null +++ b/frontend/providers/workorder/src/components/AppStatusTag/index.tsx @@ -0,0 +1,38 @@ +import { StatusMap } from '@/constants/workorder'; +import { WorkOrderStatus } from '@/types/workorder'; +import { Box, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +const AppStatusTag = ({ + status, + showBorder = false +}: { + status: WorkOrderStatus; + showBorder: boolean; +}) => { + const { t } = useTranslation(); + const statusMap = StatusMap[status]; + + return ( + + + + {t(statusMap.label)} + + + ); +}; + +export default AppStatusTag; diff --git a/frontend/providers/workorder/src/components/FileSelect/index.tsx b/frontend/providers/workorder/src/components/FileSelect/index.tsx new file mode 100644 index 000000000..3f1a7fec5 --- /dev/null +++ b/frontend/providers/workorder/src/components/FileSelect/index.tsx @@ -0,0 +1,255 @@ +import { useLoading } from '@/hooks/useLoading'; +import { useSelectFile } from '@/hooks/useSelectFile'; +import { useToast } from '@/hooks/useToast'; +import { Box, Center, Flex, Icon, Text, type BoxProps } from '@chakra-ui/react'; +import { customAlphabet } from 'nanoid'; +import { useTranslation } from 'next-i18next'; +import { DragEvent, useCallback, useState } from 'react'; + +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12); + +export const FileImgs = [ + { + reg: /\.yaml$/i, + src: ( + + + + ) + }, + { + reg: /\.(svg|png|jpg|jpeg|gif|bmp)$/i, + src: ( + + + + + + ) + } +]; + +export type FileItemType = { + id: string; + filename: string; + text?: string; + file: File; + icon: JSX.Element; +}; +interface Props extends BoxProps { + fileExtension: string; + files: FileItemType[]; + setFiles: React.Dispatch>; +} + +const FileSelect = ({ fileExtension, setFiles, files, ...BoxStyle }: Props) => { + const { Loading: FileSelectLoading } = useLoading(); + const { t } = useTranslation(); + const { toast } = useToast(); + const [isDragging, setIsDragging] = useState(false); + const [selecting, setSelecting] = useState(false); + + const { File, onOpen } = useSelectFile({ + fileType: fileExtension, + multiple: true + }); + + const onSelectFile = useCallback( + async (files: File[]) => { + setSelecting(true); + try { + // Parse file by file + let promise = Promise.resolve([]); + files.forEach((file) => { + promise = promise.then(async (result) => { + let icon = FileImgs.find((item) => item.reg.test(file.name))?.src; + if (!icon) { + icon = FileImgs[0].src; + } + const fileItem: FileItemType = { + id: nanoid(), + filename: file.name, + icon, + file: file + }; + return [...result, fileItem]; + }); + }); + const chunkFiles = await promise; + console.log(chunkFiles); + + setFiles(chunkFiles); + } catch (error: any) { + console.log(error); + toast({ + title: typeof error === 'string' ? error : '解析文件失败', + status: 'error' + }); + } + setSelecting(false); + }, + [setFiles, toast] + ); + + const handleDragEnter = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = useCallback( + async (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const items = e.dataTransfer.items; + const fileList: File[] = []; + + if (e.dataTransfer.items.length <= 1) { + const traverseFileTree = async (item: any) => { + return new Promise((resolve, reject) => { + if (item.isFile) { + item.file((file: File) => { + fileList.push(file); + resolve(); + }); + } else if (item.isDirectory) { + const dirReader = item.createReader(); + dirReader.readEntries(async (entries: any[]) => { + for (let i = 0; i < entries.length; i++) { + await traverseFileTree(entries[i]); + } + resolve(); + }); + } + }); + }; + for (let i = 0; i < items.length; i++) { + const item = items[i].webkitGetAsEntry(); + if (item) { + await traverseFileTree(item); + } + } + } else { + const files = Array.from(e.dataTransfer.files); + let isErr = files.some((item) => item.type === ''); + if (isErr) { + return toast({ + title: t('file.upload error description'), + status: 'error' + }); + } + for (let i = 0; i < files.length; i++) { + fileList.push(files[i]); + } + } + onSelectFile(fileList); + }, + [onSelectFile, t, toast] + ); + + return ( + + + + + + + {t('Upload File')} + + {/* + {files.map((item) => ( + + {item.icon} + {item.filename} + { + setFiles((state) => state.filter((file) => file.id !== item.id)); + }} + > + + + + ))} + */} + + + + + + {files.map((item) => ( + +
+ {item.icon} +
+ {item.filename} + { + setFiles((state) => state.filter((file) => file.id !== item.id)); + }} + > + + +
+ ))} +
+
+ ); +}; + +export default FileSelect; diff --git a/frontend/providers/workorder/src/components/Icon/icons/analyze.svg b/frontend/providers/workorder/src/components/Icon/icons/analyze.svg new file mode 100644 index 000000000..503178647 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/analyze.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg b/frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg new file mode 100644 index 000000000..7905b5380 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/arrowLeft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/change.svg b/frontend/providers/workorder/src/components/Icon/icons/change.svg new file mode 100644 index 000000000..d81483220 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/change.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/close.svg b/frontend/providers/workorder/src/components/Icon/icons/close.svg new file mode 100644 index 000000000..62fb9a2a8 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/close.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/connection.svg b/frontend/providers/workorder/src/components/Icon/icons/connection.svg new file mode 100644 index 000000000..393e9e2eb --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/connection.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/continue.svg b/frontend/providers/workorder/src/components/Icon/icons/continue.svg new file mode 100644 index 000000000..998357d4a --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/continue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/copy.svg b/frontend/providers/workorder/src/components/Icon/icons/copy.svg new file mode 100644 index 000000000..bfd38df8e --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg b/frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg new file mode 100644 index 000000000..9f8212841 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/defaultAvator.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/delete.svg b/frontend/providers/workorder/src/components/Icon/icons/delete.svg new file mode 100644 index 000000000..11907eec5 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/detail.svg b/frontend/providers/workorder/src/components/Icon/icons/detail.svg new file mode 100644 index 000000000..380ce9fff --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/detail.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/download.svg b/frontend/providers/workorder/src/components/Icon/icons/download.svg new file mode 100644 index 000000000..55919030d --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/formInfo.svg b/frontend/providers/workorder/src/components/Icon/icons/formInfo.svg new file mode 100644 index 000000000..164e7a19d --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/formInfo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/info.svg b/frontend/providers/workorder/src/components/Icon/icons/info.svg new file mode 100644 index 000000000..5f81980ab --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/link.svg b/frontend/providers/workorder/src/components/Icon/icons/link.svg new file mode 100644 index 000000000..126255b73 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/link.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/log.svg b/frontend/providers/workorder/src/components/Icon/icons/log.svg new file mode 100644 index 000000000..7dedd37e5 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/logo.svg b/frontend/providers/workorder/src/components/Icon/icons/logo.svg new file mode 100644 index 000000000..d826088c3 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/providers/workorder/src/components/Icon/icons/more.svg b/frontend/providers/workorder/src/components/Icon/icons/more.svg new file mode 100644 index 000000000..bc3ddc9e9 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/noEvents.svg b/frontend/providers/workorder/src/components/Icon/icons/noEvents.svg new file mode 100644 index 000000000..9665a7024 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/noEvents.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/pause.svg b/frontend/providers/workorder/src/components/Icon/icons/pause.svg new file mode 100644 index 000000000..8352927e1 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/plus.svg b/frontend/providers/workorder/src/components/Icon/icons/plus.svg new file mode 100644 index 000000000..828a964b1 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/podList.svg b/frontend/providers/workorder/src/components/Icon/icons/podList.svg new file mode 100644 index 000000000..6be85b62a --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/podList.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/read.svg b/frontend/providers/workorder/src/components/Icon/icons/read.svg new file mode 100644 index 000000000..3140dd585 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/read.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/restart.svg b/frontend/providers/workorder/src/components/Icon/icons/restart.svg new file mode 100644 index 000000000..de42eb256 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/restart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/restore.svg b/frontend/providers/workorder/src/components/Icon/icons/restore.svg new file mode 100644 index 000000000..2e89f43b6 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/robot.svg b/frontend/providers/workorder/src/components/Icon/icons/robot.svg new file mode 100644 index 000000000..f02495c3d --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/robot.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg b/frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg new file mode 100644 index 000000000..9c9f24a9b --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/sealosAvator.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/settings.svg b/frontend/providers/workorder/src/components/Icon/icons/settings.svg new file mode 100644 index 000000000..3cd5c9e17 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/start.svg b/frontend/providers/workorder/src/components/Icon/icons/start.svg new file mode 100644 index 000000000..8aa2f2981 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg b/frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg new file mode 100644 index 000000000..9716b6471 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/statusDetail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/terminal.svg b/frontend/providers/workorder/src/components/Icon/icons/terminal.svg new file mode 100644 index 000000000..db9147be9 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/unread.svg b/frontend/providers/workorder/src/components/Icon/icons/unread.svg new file mode 100644 index 000000000..29403d117 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/unread.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/icons/warning.svg b/frontend/providers/workorder/src/components/Icon/icons/warning.svg new file mode 100644 index 000000000..104b2dba1 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Icon/index.tsx b/frontend/providers/workorder/src/components/Icon/index.tsx new file mode 100644 index 000000000..9135d9fd1 --- /dev/null +++ b/frontend/providers/workorder/src/components/Icon/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import type { IconProps } from '@chakra-ui/react'; +import { Icon } from '@chakra-ui/react'; + +const map = { + more: require('./icons/more.svg').default, + podList: require('./icons/podList.svg').default, + arrowLeft: require('./icons/arrowLeft.svg').default, + plus: require('./icons/plus.svg').default, + delete: require('./icons/delete.svg').default, + restart: require('./icons/restart.svg').default, + start: require('./icons/start.svg').default, + pause: require('./icons/pause.svg').default, + detail: require('./icons/detail.svg').default, + logo: require('./icons/logo.svg').default, + change: require('./icons/change.svg').default, + formInfo: require('./icons/formInfo.svg').default, + settings: require('./icons/settings.svg').default, + copy: require('./icons/copy.svg').default, + continue: require('./icons/continue.svg').default, + noEvents: require('./icons/noEvents.svg').default, + warning: require('./icons/warning.svg').default, + analyze: require('./icons/analyze.svg').default, + terminal: require('./icons/terminal.svg').default, + log: require('./icons/log.svg').default, + statusDetail: require('./icons/statusDetail.svg').default, + read: require('./icons/read.svg').default, + unread: require('./icons/unread.svg').default, + connection: require('./icons/connection.svg').default, + info: require('./icons/info.svg').default, + restore: require('./icons/restore.svg').default, + download: require('./icons/download.svg').default, + close: require('./icons/close.svg').default, + robot: require('./icons/robot.svg').default, + sealosAvator: require('./icons/sealosAvator.svg').default, + defaultAvator: require('./icons/defaultAvator.svg').default, + link: require('./icons/link.svg').default +}; + +const MyIcon = ({ + name, + w = 'auto', + h = 'auto', + ...props +}: { name: keyof typeof map } & IconProps) => { + return map[name] ? ( + + ) : null; +}; + +export default MyIcon; diff --git a/frontend/providers/workorder/src/components/Markdown/CodeLight.tsx b/frontend/providers/workorder/src/components/Markdown/CodeLight.tsx new file mode 100644 index 000000000..a1354b477 --- /dev/null +++ b/frontend/providers/workorder/src/components/Markdown/CodeLight.tsx @@ -0,0 +1,345 @@ +import React, { useMemo } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Box, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useCopyData } from '@/utils/tools'; +import MyIcon from '../Icon'; + +const codeLight: { [key: string]: React.CSSProperties } = { + 'code[class*=language-]': { + color: '#d4d4d4', + textShadow: 'none', + direction: 'ltr', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + lineHeight: '1.5', + MozTabSize: '4', + OTabSize: '4', + tabSize: '4', + WebkitHyphens: 'none', + MozHyphens: 'none', + msHyphens: 'none', + hyphens: 'none' + }, + 'pre[class*=language-]': { + color: '#d4d4d4', + textShadow: 'none', + direction: 'ltr', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + lineHeight: '1.5', + MozTabSize: '4', + OTabSize: '4', + tabSize: '4', + WebkitHyphens: 'none', + MozHyphens: 'none', + msHyphens: 'none', + hyphens: 'none', + padding: '1em', + margin: '.5em 0', + overflow: 'auto', + background: '#1e1e1e' + }, + 'code[class*=language-] ::selection': { + textShadow: 'none', + background: '#264f78' + }, + 'code[class*=language-]::selection': { + textShadow: 'none', + background: '#264f78' + }, + 'pre[class*=language-] ::selection': { + textShadow: 'none', + background: '#264f78' + }, + 'pre[class*=language-]::selection': { + textShadow: 'none', + background: '#264f78' + }, + ':not(pre)>code[class*=language-]': { + padding: '.1em .3em', + borderRadius: '.3em', + color: '#db4c69', + background: '#1e1e1e' + }, + '.namespace': { + opacity: '0.7' + }, + 'doctype.doctype-tag': { + color: '#569cd6' + }, + 'doctype.name': { + color: '#9cdcfe' + }, + comment: { + color: '#6a9955' + }, + prolog: { + color: '#6a9955' + }, + '.language-html .language-css .token.punctuation': { + color: '#d4d4d4' + }, + '.language-html .language-javascript .token.punctuation': { + color: '#d4d4d4' + }, + punctuation: { + color: '#d4d4d4' + }, + boolean: { + color: '#569cd6' + }, + constant: { + color: '#9cdcfe' + }, + inserted: { + color: '#b5cea8' + }, + number: { + color: '#b5cea8' + }, + property: { + color: '#9cdcfe' + }, + symbol: { + color: '#b5cea8' + }, + tag: { + color: '#569cd6' + }, + unit: { + color: '#b5cea8' + }, + 'attr-name': { + color: '#9cdcfe' + }, + builtin: { + color: '#ce9178' + }, + char: { + color: '#ce9178' + }, + deleted: { + color: '#ce9178' + }, + selector: { + color: '#d7ba7d' + }, + string: { + color: '#ce9178' + }, + '.language-css .token.string.url': { + textDecoration: 'underline' + }, + entity: { + color: '#569cd6' + }, + operator: { + color: '#d4d4d4' + }, + 'operator.arrow': { + color: '#569cd6' + }, + atrule: { + color: '#ce9178' + }, + 'atrule.rule': { + color: '#c586c0' + }, + 'atrule.url': { + color: '#9cdcfe' + }, + 'atrule.url.function': { + color: '#dcdcaa' + }, + 'atrule.url.punctuation': { + color: '#d4d4d4' + }, + keyword: { + color: '#569cd6' + }, + 'keyword.control-flow': { + color: '#c586c0' + }, + 'keyword.module': { + color: '#c586c0' + }, + function: { + color: '#dcdcaa' + }, + 'function.maybe-class-name': { + color: '#dcdcaa' + }, + regex: { + color: '#d16969' + }, + important: { + color: '#569cd6' + }, + italic: { + fontStyle: 'italic' + }, + 'class-name': { + color: '#4ec9b0' + }, + 'maybe-class-name': { + color: '#4ec9b0' + }, + console: { + color: '#9cdcfe' + }, + parameter: { + color: '#9cdcfe' + }, + interpolation: { + color: '#9cdcfe' + }, + 'punctuation.interpolation-punctuation': { + color: '#569cd6' + }, + 'exports.maybe-class-name': { + color: '#9cdcfe' + }, + 'imports.maybe-class-name': { + color: '#9cdcfe' + }, + variable: { + color: '#9cdcfe' + }, + escape: { + color: '#d7ba7d' + }, + 'tag.punctuation': { + color: 'grey' + }, + cdata: { + color: 'grey' + }, + 'attr-value': { + color: '#ce9178' + }, + 'attr-value.punctuation': { + color: '#ce9178' + }, + 'attr-value.punctuation.attr-equals': { + color: '#d4d4d4' + }, + namespace: { + color: '#4ec9b0' + }, + 'code[class*=language-javascript]': { + color: '#9cdcfe' + }, + 'code[class*=language-jsx]': { + color: '#9cdcfe' + }, + 'code[class*=language-tsx]': { + color: '#9cdcfe' + }, + 'code[class*=language-typescript]': { + color: '#9cdcfe' + }, + 'pre[class*=language-javascript]': { + color: '#9cdcfe' + }, + 'pre[class*=language-jsx]': { + color: '#9cdcfe' + }, + 'pre[class*=language-tsx]': { + color: '#9cdcfe' + }, + 'pre[class*=language-typescript]': { + color: '#9cdcfe' + }, + 'code[class*=language-css]': { + color: '#ce9178' + }, + 'pre[class*=language-css]': { + color: '#ce9178' + }, + 'code[class*=language-html]': { + color: '#d4d4d4' + }, + 'pre[class*=language-html]': { + color: '#d4d4d4' + }, + '.language-regex .token.anchor': { + color: '#dcdcaa' + }, + '.language-html .token.punctuation': { + color: 'grey' + }, + 'pre[class*=language-]>code[class*=language-]': { + position: 'relative', + zIndex: '1' + }, + '.line-highlight.line-highlight': { + background: '#f7ebc6', + boxShadow: 'inset 5px 0 0 #f7d87c', + zIndex: '0' + } +}; + +const CodeLight = ({ + children, + className, + inline, + match +}: { + children: React.ReactNode & React.ReactNode[]; + className?: string; + inline?: boolean; + match: RegExpExecArray | null; +}) => { + const { t } = useTranslation(); + const { copyData } = useCopyData(); + + if (!inline) { + const codeBoxName = useMemo(() => { + const input = match?.['input'] || ''; + if (!input) return match?.[1]; + + const splitInput = input.split('#'); + return splitInput[1] || match?.[1]; + }, [match]); + + return ( + + + {codeBoxName} + copyData(String(children))} alignItems={'center'}> + + {t('Copy')} + + + + {String(children).replace(/ /g, ' ')} + + + ); + } + + return {children}; +}; + +export default React.memo(CodeLight); diff --git a/frontend/providers/workorder/src/components/Markdown/img/Image.tsx b/frontend/providers/workorder/src/components/Markdown/img/Image.tsx new file mode 100644 index 000000000..557082f79 --- /dev/null +++ b/frontend/providers/workorder/src/components/Markdown/img/Image.tsx @@ -0,0 +1,74 @@ +import { + Image, + Modal, + ModalCloseButton, + ModalContent, + ModalOverlay, + useDisclosure +} from '@chakra-ui/react'; +import React, { WheelEventHandler, useState } from 'react'; + +const MdImage = ({ src }: { src?: string }) => { + const [isLoading, setIsLoading] = useState(true); + const [succeed, setSucceed] = useState(false); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [scale, setScale] = useState(1); + + const handleWheel: WheelEventHandler = (e) => { + setScale((prevScale) => { + const newScale = prevScale + e.deltaY * 0.5 * -0.01; + if (newScale < 0.5) return 0.5; + if (newScale > 10) return 10; + return newScale; + }); + }; + + return ( + <> + {''} { + setIsLoading(false); + setSucceed(true); + }} + onError={() => setIsLoading(false)} + onClick={() => { + if (!succeed) return; + onOpen(); + }} + /> + + + + {''} + + + + + ); +}; + +export default React.memo(MdImage); diff --git a/frontend/providers/workorder/src/components/Markdown/index.module.scss b/frontend/providers/workorder/src/components/Markdown/index.module.scss new file mode 100644 index 000000000..684e63fde --- /dev/null +++ b/frontend/providers/workorder/src/components/Markdown/index.module.scss @@ -0,0 +1,424 @@ +.waitingAnimation > :last-child::after { + display: inline-block; + content: ''; + width: 3px; + height: 14px; + transform: translate(4px, 2px) scaleY(1.3); + background-color: var(--chakra-colors-primary-700); + animation: blink 0.6s infinite; +} + +.animation { + height: 20px; + + &::after { + display: inline-block; + content: ''; + width: 3px; + height: 14px; + transform: translate(4px, 2px) scaleY(1.3); + background-color: var(--chakra-colors-primary-700); + animation: blink 0.6s infinite; + } +} +@keyframes blink { + from, + to { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +.markdown > *:first-child { + margin-top: 0 !important; +} +.markdown > *:last-child { + margin-bottom: 0 !important; +} +.markdown a.absent { + color: #cc0000; +} +.markdown a.anchor { + bottom: 0; + cursor: pointer; + display: block; + left: 0; + margin-left: -30px; + padding-left: 30px; + position: absolute; + top: 0; +} +.markdown h1, +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + cursor: text; + font-weight: bold; + margin: 10px 0; + padding: 0; + position: relative; +} +.markdown h1 .mini-icon-link, +.markdown h2 .mini-icon-link, +.markdown h3 .mini-icon-link, +.markdown h4 .mini-icon-link, +.markdown h5 .mini-icon-link, +.markdown h6 .mini-icon-link { + display: none; +} +.markdown h1:hover a.anchor, +.markdown h2:hover a.anchor, +.markdown h3:hover a.anchor, +.markdown h4:hover a.anchor, +.markdown h5:hover a.anchor, +.markdown h6:hover a.anchor { + line-height: 1; + margin-left: -22px; + padding-left: 0; + text-decoration: none; + top: 15%; +} +.markdown h1:hover a.anchor .mini-icon-link, +.markdown h2:hover a.anchor .mini-icon-link, +.markdown h3:hover a.anchor .mini-icon-link, +.markdown h4:hover a.anchor .mini-icon-link, +.markdown h5:hover a.anchor .mini-icon-link, +.markdown h6:hover a.anchor .mini-icon-link { + display: inline-block; +} +.markdown h1 tt, +.markdown h1 code, +.markdown h2 tt, +.markdown h2 code, +.markdown h3 tt, +.markdown h3 code, +.markdown h4 tt, +.markdown h4 code, +.markdown h5 tt, +.markdown h5 code, +.markdown h6 tt, +.markdown h6 code { + font-size: inherit; +} +.markdown h1 { + font-size: 28px; +} +.markdown h2 { + font-size: 24px; +} +.markdown h3 { + font-size: 18px; +} +.markdown h4 { + font-size: 16px; +} +.markdown h5 { + font-size: 14px; +} +.markdown h6 { + font-size: 12px; +} +.markdown p, +.markdown blockquote, +.markdown ul, +.markdown ol, +.markdown dl, +.markdown table, +.markdown pre { + margin: 14px 0; +} +.markdown > h2:first-child, +.markdown > h1:first-child, +.markdown > h1:first-child + h2, +.markdown > h3:first-child, +.markdown > h4:first-child, +.markdown > h5:first-child, +.markdown > h6:first-child { + margin-top: 0; + padding-top: 0; +} +.markdown a:first-child h1, +.markdown a:first-child h2, +.markdown a:first-child h3, +.markdown a:first-child h4, +.markdown a:first-child h5, +.markdown a:first-child h6 { + margin-top: 0; + padding-top: 0; +} +.markdown h1 + p, +.markdown h2 + p, +.markdown h3 + p, +.markdown h4 + p, +.markdown h5 + p, +.markdown h6 + p { + margin-top: 0; +} +.markdown li p.first { + display: inline-block; +} +.markdown ul, +.markdown ol { + padding-left: 2em; +} +.markdown ul.no-list, +.markdown ol.no-list { + list-style-type: none; + padding: 0; +} +.markdown ul li > *:first-child, +.markdown ol li > *:first-child { + margin-top: 0; +} +.markdown ul, +.markdown ol { + padding-left: 14px; +} +.markdown dl { + padding: 0; +} +.markdown dl dt { + font-size: 14px; + font-style: italic; + font-weight: bold; + margin: 15px 0 5px; + padding: 0; +} +.markdown dl dt:first-child { + padding: 0; +} +.markdown dl dt > *:first-child { + margin-top: 0; +} +.markdown dl dt > *:last-child { + margin-bottom: 0; +} +.markdown dl dd { + margin: 0 0 15px; + padding: 0 15px; +} +.markdown dl dd > *:first-child { + margin-top: 0; +} +.markdown dl dd > *:last-child { + margin-bottom: 0; +} +.markdown blockquote { + border-left: 4px solid #dddddd; + color: #777777; + padding: 0 15px; +} +.markdown blockquote > *:first-child { + margin-top: 0; +} +.markdown blockquote > *:last-child { + margin-bottom: 0; +} +.markdown table { + width: 100%; +} +.markdown table th { + font-weight: bold; +} +.markdown table th, +.markdown table td { + padding: 6px 13px; +} +.markdown table tr { + background-color: #ffffff; +} +.markdown table tr:nth-child(2n) { + background-color: #f0f0f0; +} +.markdown img { + max-width: 100%; +} +.markdown span.frame { + display: block; + overflow: hidden; +} +.markdown span.frame > span { + border: 1px solid #dddddd; + display: block; + float: left; + margin: 13px 0 0; + overflow: hidden; + padding: 7px; + width: auto; +} +.markdown span.frame span img { + display: block; + float: left; +} +.markdown span.frame span span { + clear: both; + color: #333333; + display: block; + padding: 5px 0 0; +} +.markdown span.align-center { + clear: both; + display: block; + overflow: hidden; +} +.markdown span.align-center > span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} +.markdown span.align-center span img { + margin: 0 auto; + text-align: center; +} +.markdown span.align-right { + clear: both; + display: block; + overflow: hidden; +} +.markdown span.align-right > span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} +.markdown span.align-right span img { + margin: 0; + text-align: right; +} +.markdown span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} +.markdown span.float-left span { + margin: 13px 0 0; +} +.markdown span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} +.markdown span.float-right > span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} +.markdown code, +.markdown tt { + border: 1px solid #dee0e2; + background-color: #f4f6f8; + border-radius: 3px; + margin: 0 2px; + padding: 0 5px; +} +.markdown pre > code { + background: none repeat scroll 0 0 transparent; + border: medium none; + margin: 0; + padding: 0; +} +.markdown .highlight pre, +.markdown pre { + border: 1px solid #cccccc; + border-radius: 3px 3px 3px 3px; + font-size: max(0.9em, 14px); + line-height: 19px; + overflow: auto; + padding: 6px 10px; +} +.markdown pre code, +.markdown pre tt { + background-color: transparent; + border: medium none; +} +.markdown hr { + margin: 10px 0; +} +.markdown { + tab-size: 4; + word-spacing: normal; + width: 100%; + + * { + word-break: break-word; + } + + pre { + display: block; + width: 100%; + padding: 15px; + margin: 0; + border: none; + border-radius: 0; + background-color: #292b33 !important; + overflow-x: auto; + color: #fff; + } + + pre code { + background-color: #292b33 !important; + width: 100%; + } + + a { + text-decoration: underline; + color: var(--chakra-colors-primary-700); + } + + table { + border-collapse: separate; + border-spacing: 0px; + color: var(--chakra-colors-gray-700); + + thead tr:first-child th { + border-bottom-width: 1px; + border-left-width: 1px; + border-top-width: 1px; + border-color: #ccc; + background-color: rgba(236, 236, 241, 0.2); + overflow: hidden; + + &:first-child { + border-top-left-radius: 0.375rem; + } + &:last-child { + border-right-width: 1px; + border-top-right-radius: 0.375rem; + } + } + + td { + border-bottom-width: 1px; + border-left-width: 1px; + border-color: #ccc; + + &:last-of-type { + border-right-width: 1px; + } + } + + tbody tr:last-child { + overflow: hidden; + td { + &:first-child { + border-bottom-left-radius: 0.375rem; + } + &:last-child { + border-bottom-right-radius: 0.375rem; + } + } + } + } +} + +.mermaid { + overflow-x: auto; +} diff --git a/frontend/providers/workorder/src/components/Markdown/index.tsx b/frontend/providers/workorder/src/components/Markdown/index.tsx new file mode 100644 index 000000000..54aebfeba --- /dev/null +++ b/frontend/providers/workorder/src/components/Markdown/index.tsx @@ -0,0 +1,77 @@ +import dynamic from 'next/dynamic'; +import React, { useMemo } from 'react'; +import ReactMarkdown from 'react-markdown'; +import RemarkBreaks from 'remark-breaks'; +import RemarkGfm from 'remark-gfm'; +import styles from './index.module.scss'; + +const CodeLight = dynamic(() => import('./CodeLight')); +const MdImage = dynamic(() => import('./img/Image')); + +export enum CodeClassName { + guide = 'guide', + questionGuide = 'questionGuide', + mermaid = 'mermaid', + echarts = 'echarts', + quote = 'quote', + files = 'files' +} + +const Markdown = ({ + source = '', + showAnimation = false +}: { + source?: string; + showAnimation?: boolean; +}) => { + const components = useMemo( + () => ({ + img: Image, + pre: 'div', + p: (pProps: any) =>

, + code: Code + }), + [] + ); + + const formatSource = source + .replace(/\\n/g, '\n ') + .replace(/(http[s]?:\/\/[^\s,。]+)([。,])/g, '$1 $2') + .replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1'); + + return ( + + {formatSource} + + ); +}; + +export default React.memo(Markdown); + +const Code = React.memo(function Code(e: any) { + const { inline, className, children } = e; + + const match = /language-(\w+)/.exec(className || ''); + + const Component = useMemo(() => { + return ( + + {children} + + ); + }, [className, inline, match, children]); + + return Component; +}); + +const Image = React.memo(function Image({ src }: { src?: string }) { + return ; +}); diff --git a/frontend/providers/workorder/src/components/Menu/index.tsx b/frontend/providers/workorder/src/components/Menu/index.tsx new file mode 100644 index 000000000..7740239c4 --- /dev/null +++ b/frontend/providers/workorder/src/components/Menu/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Menu, MenuList, MenuItem } from '@chakra-ui/react'; + +interface Props { + width: number; + Button: React.ReactNode; + menuList: { + isActive?: boolean; + child: React.ReactNode; + onClick: () => void; + }[]; +} + +const MyMenu = ({ width, Button, menuList }: Props) => { + const menuItemStyles = { + borderRadius: 'sm', + py: 2, + display: 'flex', + alignItems: 'center', + _hover: { + backgroundColor: 'myWhite.600', + color: 'hover.blue' + } + }; + + return ( +

+ {Button} + + {menuList.map((item, i) => ( + + {item.child} + + ))} + + + ); +}; + +export default MyMenu; diff --git a/frontend/providers/workorder/src/components/MonitorTabs/index.tsx b/frontend/providers/workorder/src/components/MonitorTabs/index.tsx new file mode 100644 index 000000000..f037d7d6c --- /dev/null +++ b/frontend/providers/workorder/src/components/MonitorTabs/index.tsx @@ -0,0 +1,97 @@ +import React, { useMemo } from 'react'; +import { Box, Flex, Grid } from '@chakra-ui/react'; +import type { GridProps } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +// @ts-ignore +interface Props extends GridProps { + list: { id: string; label: string }[]; + activeId: string; + size?: 'sm' | 'md' | 'lg'; + onChange: (id: string) => void; +} + +const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { + const { t } = useTranslation(); + + const sizeMap = useMemo(() => { + switch (size) { + case 'sm': + return { + fontSize: 'sm', + outP: '3px', + inlineP: 1 + }; + case 'md': + return { + fontSize: 'md', + outP: '4px', + inlineP: 2 + }; + case 'lg': + return { + fontSize: 'lg', + outP: '5px', + inlineP: 3 + }; + } + }, [size]); + + const activeIndex = useMemo( + () => list.findIndex((item) => item.id === activeId), + [activeId, list] + ); + + return ( + + {list.map((item, i) => ( + { + if (activeId === item.id) return; + onChange(item.id); + }} + > + {t(item.label)} + + ))} + + ); +}; + +export default Tabs; diff --git a/frontend/providers/workorder/src/components/MyTooltip/index.tsx b/frontend/providers/workorder/src/components/MyTooltip/index.tsx new file mode 100644 index 000000000..d51261c56 --- /dev/null +++ b/frontend/providers/workorder/src/components/MyTooltip/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Tooltip, TooltipProps } from '@chakra-ui/react'; + +const MyTooltip = ({ children, ...props }: TooltipProps) => { + return ( + + {children} + + ); +}; + +export default MyTooltip; diff --git a/frontend/providers/workorder/src/components/Pagination/index.tsx b/frontend/providers/workorder/src/components/Pagination/index.tsx new file mode 100644 index 000000000..57650ae25 --- /dev/null +++ b/frontend/providers/workorder/src/components/Pagination/index.tsx @@ -0,0 +1,132 @@ +import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; +import { Dispatch, SetStateAction, useState } from 'react'; + +type PaginationProps = { + totalItems: number; + itemsPerPage: number; + currentPage: number; + setCurrentPage: Dispatch>; +}; + +export default function Pagination({ + totalItems, + itemsPerPage, + currentPage = 1, + setCurrentPage +}: PaginationProps) { + const totalPage = Math.ceil(totalItems / itemsPerPage); + + const goToPage = (page: number) => { + if (page >= 1 && page <= totalPage) { + setCurrentPage(page); + } + }; + + const handlePrevPage = () => { + goToPage(currentPage - 1); + }; + + const handleNextPage = () => { + goToPage(currentPage + 1); + }; + + const buttonStyle: FlexProps = { + justifyContent: 'center', + alignItems: 'center', + w: '24px', + h: '24px', + borderRadius: '50%' + }; + + return ( + + Total: + {totalItems} + + goToPage(1)} + {...buttonStyle} + {...{ + bg: currentPage === 1 ? '#EDF0F2' : '#EDEFF1', + cursor: currentPage === 1 ? 'not-allowed' : 'pointer', + opacity: currentPage === 1 ? '0.5' : '1' + }} + > + + + + + + + + + + + + {currentPage}/{totalPage} + + + + + + + + goToPage(totalPage)} + > + + + + + + {itemsPerPage} + /Page + + ); +} diff --git a/frontend/providers/workorder/src/components/RangeDate/index.tsx b/frontend/providers/workorder/src/components/RangeDate/index.tsx new file mode 100644 index 000000000..c46b79ae2 --- /dev/null +++ b/frontend/providers/workorder/src/components/RangeDate/index.tsx @@ -0,0 +1,70 @@ +import { + Flex, + Input, + Popover, + PopoverTrigger, + Img, + PopoverContent, + Button, + Box, + useTheme +} from '@chakra-ui/react'; +import { format, parse, isValid, isAfter, isBefore } from 'date-fns'; +import { useState, ChangeEventHandler, useEffect, useMemo } from 'react'; +import { DateRange, SelectRangeEventHandler, DayPicker } from 'react-day-picker'; +import { useQueryClient } from '@tanstack/react-query'; +import { CalendarIcon } from '@chakra-ui/icons'; + +export default function RangeDate({ + isDisabled = false, + defaultRange = { from: new Date(), to: new Date() } +}: { + isDisabled?: boolean; + defaultRange?: DateRange; +}) { + const [selectedRange, setSelectedRange] = useState(defaultRange); + + const onClose = () => {}; + + return ( + + + + + + { + if (e) { + setSelectedRange(e); + } else { + setSelectedRange((state) => ({ + from: state.from, + to: state.from + })); + } + }} + /> + + + ); +} diff --git a/frontend/providers/workorder/src/components/RangeDate/style.module.scss b/frontend/providers/workorder/src/components/RangeDate/style.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/providers/workorder/src/components/Select/index.tsx b/frontend/providers/workorder/src/components/Select/index.tsx new file mode 100644 index 000000000..83f108226 --- /dev/null +++ b/frontend/providers/workorder/src/components/Select/index.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure, Box } from '@chakra-ui/react'; +import type { ButtonProps } from '@chakra-ui/react'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { useTranslation } from 'next-i18next'; + +interface Props extends ButtonProps { + isDisabled?: boolean; + value?: string; + placeholder?: string; + list: { + label: string; + id: string; + }[]; + width?: number | string; + icon?: React.ReactNode; + onchange?: (val: string) => void; +} + +const MySelect = ({ + placeholder, + isDisabled = false, + value, + width = 'auto', + list, + onchange, + icon = , + ...props +}: Props) => { + const { t } = useTranslation(); + + const menuItemStyles = { + borderRadius: 'sm', + py: 2, + display: 'flex', + alignItems: 'center', + transform: 'translate3d(0,0,0)', + cursor: 'pointer', + px: 3, + _hover: { + backgroundColor: 'myWhite.600' + } + }; + const { isOpen, onOpen, onClose } = useDisclosure(); + + return ( + !isDisabled && onOpen()} + onClose={onClose} + > + + + + {!isDisabled && ( + + {list?.map((item) => ( + { + if (onchange && value !== item.id) { + onchange(item.id); + } + onClose(); + }} + > + {t(item.label)} + + ))} + + )} + + ); +}; + +export default React.memo(MySelect); diff --git a/frontend/providers/workorder/src/components/SelectDateRange/index.tsx b/frontend/providers/workorder/src/components/SelectDateRange/index.tsx new file mode 100644 index 000000000..635f189f4 --- /dev/null +++ b/frontend/providers/workorder/src/components/SelectDateRange/index.tsx @@ -0,0 +1,289 @@ +import { + Box, + Button, + Flex, + Icon, + Input, + Popover, + PopoverContent, + PopoverTrigger +} from '@chakra-ui/react'; +import { endOfDay, format, isAfter, isBefore, isValid, parse, startOfDay } from 'date-fns'; +import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react'; +import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker'; + +type SelectDateRangeProps = { + isDisabled?: boolean; + startTime: Date; + setStartTime: Dispatch>; + endTime: Date; + setEndTime: Dispatch>; +}; + +export default function SelectDateRange({ + isDisabled, + startTime, + setStartTime, + endTime, + setEndTime +}: SelectDateRangeProps) { + const initState = { from: startTime, to: endTime }; + + const [selectedRange, setSelectedRange] = useState(initState); + const [fromValue, setFromValue] = useState(format(initState.from, 'y-MM-dd')); + const [toValue, setToValue] = useState(format(initState.to, 'y-MM-dd')); + const [inputState, setInputState] = useState<0 | 1>(0); + const onClose = () => { + selectedRange?.from && setStartTime(startOfDay(selectedRange.from)); + selectedRange?.to && setEndTime(endOfDay(selectedRange.to)); + }; + + const handleFromChange: ChangeEventHandler = (e) => { + setFromValue(e.target.value); + const date = parse(e.target.value, 'y-MM-dd', new Date()); + if (!isValid(date)) { + return setSelectedRange({ from: undefined, to: selectedRange?.to }); + } + + if (selectedRange?.to) { + if (isAfter(date, selectedRange.to)) { + setSelectedRange({ from: selectedRange.to, to: date }); + } else { + setSelectedRange({ from: date, to: selectedRange?.to }); + } + } else { + setSelectedRange({ from: date, to: date }); + } + }; + + const handleToChange: ChangeEventHandler = (e) => { + setToValue(e.target.value); + const date = parse(e.target.value, 'y-MM-dd', new Date()); + + if (!isValid(date)) { + return setSelectedRange({ from: selectedRange?.from, to: undefined }); + } + if (selectedRange?.from) { + if (isBefore(date, selectedRange.from)) { + setSelectedRange({ from: date, to: selectedRange.from }); + } else { + setSelectedRange({ from: selectedRange?.from, to: date }); + } + } else { + setSelectedRange({ from: date, to: date }); + } + }; + + const handleRangeSelect: SelectRangeEventHandler = (range: DateRange | undefined) => { + if (range) { + let { from, to } = range; + if (inputState === 0) { + if (from === selectedRange?.from) { + from = to; + } else { + to = from; + } + setInputState(1); + } else { + setInputState(0); + } + setSelectedRange({ + from, + to + }); + if (from) { + setFromValue(format(from, 'y-MM-dd')); + } else { + setFromValue(''); + } + if (to) { + setToValue(format(to, 'y-MM-dd')); + } else { + setToValue(from ? format(from, 'y-MM-dd') : ''); + } + } else { + if (fromValue && selectedRange?.from) { + setToValue(fromValue); + setSelectedRange({ + ...selectedRange, + to: selectedRange.from + }); + setInputState(1); + } + } + }; + + const handleRangeSelectFrom: SelectRangeEventHandler = (range: DateRange | undefined) => { + if (range) { + let { from, to } = range; + if (selectedRange?.to) { + if (from) { + if (!to) { + to = from; + } else if (from === selectedRange?.from) { + from = to; + to = selectedRange.to; + } + if (isBefore(from, selectedRange.to)) { + setSelectedRange({ + ...selectedRange, + from + }); + setFromValue(format(from, 'y-MM-dd')); + } + } + } + } + }; + + const handleRangeSelectTo: SelectRangeEventHandler = (range: DateRange | undefined) => { + console.log(range, selectedRange); + if (range) { + let { from, to } = range; + if (selectedRange?.from) { + if (to) { + if (!from) { + from = to; + } else if (to === selectedRange?.to) { + to = from; + from = selectedRange.from; + } + if (isAfter(to, selectedRange.from)) { + setSelectedRange({ + ...selectedRange, + to + }); + setToValue(format(to, 'y-MM-dd')); + } + } + } + } else { + if (fromValue && selectedRange?.from) { + setToValue(fromValue); + setSelectedRange({ + ...selectedRange, + to: selectedRange.from + }); + setInputState(1); + } + } + }; + + return ( + + + + + + + + + + - + + + + + + + + + + { + setInputState(0); + onClose(); + }} + > + + + + + + + + + ); +} diff --git a/frontend/providers/workorder/src/components/Table/index.tsx b/frontend/providers/workorder/src/components/Table/index.tsx new file mode 100644 index 000000000..3914c96fc --- /dev/null +++ b/frontend/providers/workorder/src/components/Table/index.tsx @@ -0,0 +1,71 @@ +import { BoxProps, Flex, FlexProps } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +interface Props extends BoxProps { + columns: { + title: string; + dataIndex?: string; + key: string; + render?: (item: any) => JSX.Element; + ItemStyle?: FlexProps; + }[]; + data: any[]; +} + +const Table = ({ columns, data }: Props) => { + const { t } = useTranslation(); + + return ( + <> + + {columns.map((col, i) => ( + + {t(col.title)} + + ))} + + {data.map((item: any, index1) => ( + + {columns.map((col, index2) => ( + + {col.render ? col.render(item) : col.dataIndex ? `${item[col.dataIndex]}` : ''} + + ))} + + ))} + + ); +}; + +export default Table; diff --git a/frontend/providers/workorder/src/components/Tabs/index.tsx b/frontend/providers/workorder/src/components/Tabs/index.tsx new file mode 100644 index 000000000..3a91b9402 --- /dev/null +++ b/frontend/providers/workorder/src/components/Tabs/index.tsx @@ -0,0 +1,94 @@ +import React, { useMemo } from 'react'; +import { Box, Grid, useBreakpointValue } from '@chakra-ui/react'; +import type { GridProps } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +// @ts-ignore +interface Props extends GridProps { + list: { id: string; label: string }[]; + activeId: string; + size?: 'sm' | 'md' | 'lg'; + onChange: (id: string) => void; +} + +const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { + const { t } = useTranslation(); + const pxValue = useBreakpointValue({ base: '14px', lg: '20px' }); + + const sizeMap = useMemo(() => { + switch (size) { + case 'sm': + return { + fontSize: 'sm', + outP: '3px', + inlineP: 1 + }; + case 'md': + return { + fontSize: 'md', + outP: '4px', + inlineP: '4px' + }; + case 'lg': + return { + fontSize: 'lg', + outP: '5px', + inlineP: 3 + }; + } + }, [size]); + const activeIndex = useMemo( + () => list.findIndex((item) => item.id === activeId), + [activeId, list] + ); + return ( + + {list.map((item, i) => ( + { + if (activeId === item.id) return; + onChange(item.id); + }} + > + {t(item.label)} + + ))} + + ); +}; + +export default Tabs; diff --git a/frontend/providers/workorder/src/components/Tip/index.tsx b/frontend/providers/workorder/src/components/Tip/index.tsx new file mode 100644 index 000000000..de7031bb4 --- /dev/null +++ b/frontend/providers/workorder/src/components/Tip/index.tsx @@ -0,0 +1,66 @@ +import React, { useMemo } from 'react'; +import { BoxProps, Flex, Box } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +interface Props extends BoxProps { + text: string; + icon?: JSX.Element; + theme?: 'blue'; + size?: 'sm' | 'md' | 'lg'; +} + +const Tip = ({ size = 'md', text, icon, theme, ...props }: Props) => { + const { t } = useTranslation(); + + const sizeMap = useMemo(() => { + switch (size) { + case 'sm': + return { + fontSize: '12px', + px: 3, + py: 1 + }; + case 'md': + return { + fontSize: '14px', + px: 4, + py: 2 + }; + case 'lg': + return { + fontSize: '16px', + px: 5, + py: 2 + }; + default: + return {}; + } + }, [size]); + const themeMap = useMemo(() => { + switch (theme) { + case 'blue': + return { + color: '#0884DD', + bg: '#ECF8FF' + }; + default: + return { + color: '#0884DD', + bg: '#ECF8FF' + }; + } + }, [theme]); + + return ( + + {icon ? ( + + {icon} + + ) : null} + {t(text)} + + ); +}; + +export default Tip; diff --git a/frontend/providers/workorder/src/constants/theme.ts b/frontend/providers/workorder/src/constants/theme.ts new file mode 100644 index 000000000..3265f4726 --- /dev/null +++ b/frontend/providers/workorder/src/constants/theme.ts @@ -0,0 +1,334 @@ +import { + ComponentStyleConfig, + createMultiStyleConfigHelpers, + defineStyle, + defineStyleConfig, + extendTheme +} from '@chakra-ui/react'; +// @ts-ignore +import { + selectAnatomy, + switchAnatomy, + numberInputAnatomy, + checkboxAnatomy +} from '@chakra-ui/anatomy'; +const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } = + createMultiStyleConfigHelpers(selectAnatomy.keys); +const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle } = + createMultiStyleConfigHelpers(switchAnatomy.keys); +const { definePartsStyle: numInputPart, defineMultiStyleConfig: numInputMultiStyle } = + createMultiStyleConfigHelpers(numberInputAnatomy.keys); +const { definePartsStyle: checkboxPart, defineMultiStyleConfig: checkboxStyle } = + createMultiStyleConfigHelpers(checkboxAnatomy.keys); + +const Button = defineStyleConfig({ + baseStyle: { + _active: { + transform: 'scale(0.98)' + } + }, + sizes: { + xs: { + fontSize: 'xs', + px: 3, + py: 0, + fontWeight: 'normal', + height: '22px', + borderRadius: '2px' + }, + sm: { + fontSize: 'sm', + px: 3, + py: 0, + fontWeight: 'normal', + height: '26px', + borderRadius: '2px' + }, + md: { + fontSize: 'md', + px: 6, + py: 0, + height: '34px', + fontWeight: 'normal', + borderRadius: '4px' + }, + lg: { + fontSize: 'lg', + px: 8, + py: 0, + height: '42px', + fontWeight: 'normal', + borderRadius: '8px' + } + }, + variants: { + primary: { + bg: 'myGray.900', + color: 'white', + _hover: { + bg: 'myGray.700 !important' + } + }, + base: { + bg: 'myWhite.600', + color: 'myGray.900', + border: '1px solid', + borderColor: 'myGray.100', + _hover: { + color: 'hover.blue' + } + } + }, + defaultProps: { + size: 'md', + // @ts-ignore + variant: 'outline' + } +}); + +const Input: ComponentStyleConfig = { + baseStyle: {}, + variants: { + outline: { + field: { + bg: 'myWhite.300', + border: '1px solid', + borderRadius: 'base', + borderColor: 'myGray.200', + _focus: { + borderColor: 'myBlue.600', + boxShadow: '0px 0px 4px #A8DBFF', + bg: 'white' + }, + _disabled: { + color: 'myGray.400', + bg: 'myWhite.300' + } + } + } + }, + defaultProps: { + size: 'md', + variant: 'outline' + } +}; +const NumberInput = numInputMultiStyle({ + variants: { + outline: numInputPart({ + field: { + bg: 'myWhite.300', + border: '1px solid', + borderRadius: 'base', + borderColor: 'myGray.200', + _focus: { + borderColor: 'myBlue.600 !important', + boxShadow: '0px 0px 4px #A8DBFF !important', + bg: 'transparent' + }, + _disabled: { + color: 'myGray.400 !important', + bg: 'myWhite.300 !important' + } + }, + stepper: { + bg: 'transparent', + border: 'none', + color: 'myGray.600', + _active: { + color: 'myBlue.600' + } + } + }) + }, + defaultProps: { + variant: 'outline' + } +}); + +const Textarea: ComponentStyleConfig = { + variants: { + outline: { + bg: 'myWhite.300', + border: '1px solid', + borderRadius: 'base', + borderColor: 'myGray.200', + _focus: { + borderColor: 'myBlue.600', + boxShadow: '0px 0px 4px #A8DBFF' + } + } + }, + + defaultProps: { + size: 'md', + variant: 'outline' + } +}; + +const Select = selectMultiStyle({ + variants: { + outline: selectPart({ + field: { + borderColor: 'myGray.100', + + _focusWithin: { + boxShadow: '0px 0px 4px #A8DBFF', + borderColor: 'myBlue.600' + } + } + }) + } +}); + +const Switch = switchMultiStyle({ + baseStyle: switchPart({ + track: { + bg: 'myGray.100', + _checked: { + bg: 'myGray.700' + } + } + }), + variants: { + deepLight: { + track: { + bg: 'myGray.200', + _checked: { + bg: 'myGray.700' + } + } + } + } +}); + +const Tooltip = defineStyleConfig({ + baseStyle: { + p: 2, + bg: 'white', + color: 'blackAlpha.800', + borderRadius: '8px', + boxShadow: '1px 1px 7px rgba(0,0,0,0.2)' + } +}); + +const Checkbox = checkboxStyle({ + baseStyle: checkboxPart({ + control: defineStyle({ + _checked: { + bg: 'black', + borderColor: 'black', + _hover: { + bg: 'black !important', + borderColor: 'black !important' + } + } + }) + }) +}); + +export const theme = extendTheme({ + styles: { + global: { + 'html, body': { + color: 'myGray.900', + fontSize: 'md', + height: '100%', + fontWeight: 400 + } + } + }, + Sizes: { + xs: '10px', + sm: '12px', + md: '14px', + lg: '16px', + xl: '16px', + '2xl': '18px', + '3xl': '20px' + }, + colors: { + divider: { + 100: '#E5E7E9' + }, + hover: { + blue: '#0884DD', + iconBlue: '#219BF4' + }, + myGray: { + 100: '#EFF0F1', + 200: '#DEE0E2', + 300: '#BDC1C5', + 400: '#9CA2A8', + 500: '#7B838B', + 600: '#5A646E', + 700: '#485058', + 800: '#363C42', + 900: '#24282C', + 1000: '#121416' + }, + myWhite: { + 100: '#FEFEFE', + 200: '#FDFDFE', + 300: '#FBFBFC', + 400: '#F8FAFB', + 500: '#F6F8F9', + 600: '#F4F6F8', + 700: '#C3C5C6', + 800: '#929495', + 900: '#626263', + 1000: '#313132' + }, + myBlue: { + 100: '#EBF7FD', + 200: '#D7EFFC', + 300: '#AFDEF9', + 400: '#86CEF5', + 500: '#5EBDF2', + 600: '#36ADEF', + 700: '#2B8ABF', + 800: '#20688F', + 900: '#164560', + 1000: '#0B2330' + }, + myRed: { + 100: '#FFEBED', + 200: '#FFD6DB', + 300: '#FFADB7', + 400: '#FF8492', + 500: '#FF5B6E', + 600: '#FF324A', + 700: '#CC283B', + 800: '#991E2C', + 900: '#66141E', + 1000: '#330A0F' + } + }, + borders: { + sm: '1px solid #EFF0F1', + base: '1px solid #DEE0E2', + md: '1px solid #BDC1C5' + }, + shadows: { + outline: '' + }, + radii: { + xs: '1px', + sm: '2px', + base: '2px', + md: '4px', + lg: '6px' + }, + fontWeights: { + bold: 500 + }, + components: { + Button, + Input, + Tooltip, + Select, + Switch, + Textarea, + NumberInput, + Checkbox + } +}); diff --git a/frontend/providers/workorder/src/constants/workorder.ts b/frontend/providers/workorder/src/constants/workorder.ts new file mode 100644 index 000000000..581a58a4d --- /dev/null +++ b/frontend/providers/workorder/src/constants/workorder.ts @@ -0,0 +1,85 @@ +import { WorkOrderStatus, WorkOrderType } from '@/types/workorder'; + +export const OrderTypeList: { + id: WorkOrderType; + label: string; +}[] = [ + { + id: WorkOrderType.AppLaunchpad, + label: 'applaunchpad' + }, + { + id: WorkOrderType.CostCenter, + label: 'costcenter' + }, + { + id: WorkOrderType.Template, + label: 'template' + }, + { + id: WorkOrderType.DBProvider, + label: 'dbprovider' + }, + { + id: WorkOrderType.CronJob, + label: 'cronjob' + }, + { + id: WorkOrderType.CloudServer, + label: 'cloudserver' + }, + { + id: WorkOrderType.ObjectStorage, + label: 'objectstorage' + }, + { + id: WorkOrderType.FunctionServer, + label: 'functionserver' + } +]; + +export const StatusMap: { + [key in WorkOrderStatus]: { + label: string; + value: WorkOrderStatus; + color: string; + backgroundColor: string; + dotColor: string; + }; +} = { + all: { + label: 'Pending', + value: WorkOrderStatus.All, + color: '#FB7C3C', + backgroundColor: '#FFF2EC', + dotColor: '#FB7C3C' + }, + pending: { + label: 'Pending', + value: WorkOrderStatus.Pending, + color: '#FB7C3C', + backgroundColor: '#FFF2EC', + dotColor: '#FB7C3C' + }, + processing: { + label: 'Processing', + value: WorkOrderStatus.Processing, + color: '#219BF4', + backgroundColor: '#EBF7FD', + dotColor: '#219BF4' + }, + completed: { + label: 'Completed', + value: WorkOrderStatus.Completed, + color: '#787A90', + backgroundColor: '#F5F5F8', + dotColor: '#787A90' + }, + deleted: { + label: 'Deleted', + value: WorkOrderStatus.Deleted, + color: '#FF5B6E', + backgroundColor: '#FFEBED', + dotColor: '#FF5B6E' + } +}; diff --git a/frontend/providers/workorder/src/hooks/useConfirm.tsx b/frontend/providers/workorder/src/hooks/useConfirm.tsx new file mode 100644 index 000000000..a08a54b83 --- /dev/null +++ b/frontend/providers/workorder/src/hooks/useConfirm.tsx @@ -0,0 +1,79 @@ +import { useCallback, useRef } from 'react'; +import { + AlertDialog, + AlertDialogBody, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogContent, + AlertDialogOverlay, + useDisclosure, + Button +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +export const useConfirm = ({ + title = 'Prompt', + content, + confirmText = 'Confirm' +}: { + title?: string; + content: string; + confirmText?: string; +}) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const { t } = useTranslation(); + const cancelRef = useRef(null); + const confirmCb = useRef(); + const cancelCb = useRef(); + + return { + openConfirm: useCallback( + (confirm?: any, cancel?: any) => { + return function () { + onOpen(); + confirmCb.current = confirm; + cancelCb.current = cancel; + }; + }, + [onOpen] + ), + ConfirmChild: useCallback( + () => ( + + + + + {t(title)} + + + {content} + + + + + + + + + ), + [confirmText, content, isOpen, onClose, t, title] + ) + }; +}; diff --git a/frontend/providers/workorder/src/hooks/useLoading.tsx b/frontend/providers/workorder/src/hooks/useLoading.tsx new file mode 100644 index 000000000..f00dac51b --- /dev/null +++ b/frontend/providers/workorder/src/hooks/useLoading.tsx @@ -0,0 +1,34 @@ +import { useState, useCallback } from 'react'; +import { Spinner, Flex } from '@chakra-ui/react'; + +export const useLoading = (props?: { defaultLoading: boolean }) => { + const [isLoading, setIsLoading] = useState(props?.defaultLoading || false); + + const Loading = useCallback( + ({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => { + return ( + + + + ); + }, + [isLoading] + ); + + return { + isLoading, + setIsLoading, + Loading + }; +}; diff --git a/frontend/providers/workorder/src/hooks/useScreen.ts b/frontend/providers/workorder/src/hooks/useScreen.ts new file mode 100644 index 000000000..79af2c568 --- /dev/null +++ b/frontend/providers/workorder/src/hooks/useScreen.ts @@ -0,0 +1,25 @@ +import { useState, useEffect } from 'react'; +import { throttle } from 'lodash'; + +export function useScreen() { + const [screenWidth, setScreenWidth] = useState(1440); + + useEffect(() => { + const resize = throttle(() => { + const w = document.documentElement.clientWidth; + setScreenWidth(w); + }, 300); + window.addEventListener('resize', resize); + + const w = document.documentElement.clientWidth; + setScreenWidth(w); + + return () => { + window.removeEventListener('resize', resize); + }; + }, []); + + return { + screenWidth + }; +} diff --git a/frontend/providers/workorder/src/hooks/useSelectFile.tsx b/frontend/providers/workorder/src/hooks/useSelectFile.tsx new file mode 100644 index 000000000..9570cc642 --- /dev/null +++ b/frontend/providers/workorder/src/hooks/useSelectFile.tsx @@ -0,0 +1,44 @@ +import React, { useRef, useCallback } from 'react'; +import { Box } from '@chakra-ui/react'; +import { useToast } from './useToast'; +import { useTranslation } from 'react-i18next'; + +export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => { + const { t } = useTranslation(); + const { fileType = '*', multiple = false } = props || {}; + const { toast } = useToast(); + const SelectFileDom = useRef(null); + + const File = useCallback( + ({ onSelect }: { onSelect: (e: File[]) => void }) => ( + + { + if (!e.target.files || e.target.files?.length === 0) return; + if (e.target.files.length > 10) { + return toast({ + status: 'warning', + title: t('file.Select a maximum of 10 files') + }); + } + onSelect(Array.from(e.target.files)); + }} + /> + + ), + [fileType, multiple, t, toast] + ); + + const onOpen = useCallback(() => { + SelectFileDom.current && SelectFileDom.current.click(); + }, []); + + return { + File, + onOpen + }; +}; diff --git a/frontend/providers/workorder/src/hooks/useStore.ts b/frontend/providers/workorder/src/hooks/useStore.ts new file mode 100644 index 000000000..439fcef7d --- /dev/null +++ b/frontend/providers/workorder/src/hooks/useStore.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +const useStore = ( + store: (callback: (state: T) => unknown) => unknown, + callback: (state: T) => F +) => { + const result = store(callback) as F; + const [data, setData] = useState(); + + useEffect(() => { + setData(result); + }, [result]); + + return data; +}; + +export default useStore; diff --git a/frontend/providers/workorder/src/hooks/useToast.ts b/frontend/providers/workorder/src/hooks/useToast.ts new file mode 100644 index 000000000..f8b1cbe36 --- /dev/null +++ b/frontend/providers/workorder/src/hooks/useToast.ts @@ -0,0 +1,12 @@ +import { useToast as uToast, UseToastOptions } from '@chakra-ui/react'; + +export const useToast = (props?: UseToastOptions) => { + const toast = uToast({ + position: 'top', + ...props + }); + + return { + toast + }; +}; diff --git a/frontend/providers/workorder/src/pages/404.tsx b/frontend/providers/workorder/src/pages/404.tsx new file mode 100644 index 000000000..63f53471d --- /dev/null +++ b/frontend/providers/workorder/src/pages/404.tsx @@ -0,0 +1,13 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +const NonePage = () => { + const router = useRouter(); + useEffect(() => { + router.push('/workorders'); + }, [router]); + + return
; +}; + +export default NonePage; diff --git a/frontend/providers/workorder/src/pages/_app.tsx b/frontend/providers/workorder/src/pages/_app.tsx new file mode 100644 index 000000000..5f4e1dbdc --- /dev/null +++ b/frontend/providers/workorder/src/pages/_app.tsx @@ -0,0 +1,147 @@ +import { theme } from '@/constants/theme'; +import { useConfirm } from '@/hooks/useConfirm'; +import useEnvStore from '@/store/env'; +import { useGlobalStore } from '@/store/global'; +import useSessionStore from '@/store/session'; +import '@/styles/reset.scss'; +import { getLangStore, setLangStore } from '@/utils/cookieUtils'; +import { ChakraProvider } from '@chakra-ui/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { throttle } from 'lodash'; +import { appWithTranslation, useTranslation } from 'next-i18next'; +import type { AppProps } from 'next/app'; +import Head from 'next/head'; +import Router, { useRouter } from 'next/router'; +import NProgress from 'nprogress'; //nprogress module +import 'nprogress/nprogress.css'; +import { useEffect, useState } from 'react'; +import 'react-day-picker/dist/style.css'; +import { EVENT_NAME } from 'sealos-desktop-sdk'; +import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'; + +//Binding events. +Router.events.on('routeChangeStart', () => NProgress.start()); +Router.events.on('routeChangeComplete', () => NProgress.done()); +Router.events.on('routeChangeError', () => NProgress.done()); + +// Create a client +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false, + cacheTime: 0 + } + } +}); + +function App({ Component, pageProps }: AppProps) { + const router = useRouter(); + const { initSystemEnv } = useEnvStore(); + const { authUser, delAppSession } = useSessionStore(); + const { i18n } = useTranslation(); + const { setScreenWidth, setLastRoute } = useGlobalStore(); + const [refresh, setRefresh] = useState(false); + + const { openConfirm, ConfirmChild } = useConfirm({ + title: '跳转提示', + content: '该应用不允许单独使用,点击确认前往 Sealos Desktop 使用。' + }); + + useEffect(() => { + const response = createSealosApp(); + + (async () => { + const { domain } = await initSystemEnv(); + try { + const res = await sealosApp.getSession(); + if (!res?.token) return; + authUser(res.token); + } catch (err) { + if (process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_MOCK_USER) { + authUser(process.env.NEXT_PUBLIC_MOCK_USER); + } else { + delAppSession(); + openConfirm(() => { + window.open(`https://${domain}`, '_self'); + })(); + } + } + })(); + return response; + }, []); + + // add resize event + useEffect(() => { + const resize = throttle((e: Event) => { + const documentWidth = document.documentElement.clientWidth || document.body.clientWidth; + setScreenWidth(documentWidth); + }, 200); + window.addEventListener('resize', resize); + const documentWidth = document.documentElement.clientWidth || document.body.clientWidth; + setScreenWidth(documentWidth); + + return () => { + window.removeEventListener('resize', resize); + }; + }, [setScreenWidth]); + + // init + useEffect(() => { + const changeI18n = async (data: any) => { + const lastLang = getLangStore(); + const newLang = data.currentLanguage; + if (lastLang !== newLang) { + i18n?.changeLanguage?.(newLang); + setLangStore(newLang); + setRefresh((state) => !state); + } + }; + + (async () => { + try { + const lang = await sealosApp.getLanguage(); + changeI18n({ + currentLanguage: lang.lng + }); + } catch (error) { + changeI18n({ + currentLanguage: 'zh' + }); + } + })(); + + return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n); + }, []); + + // record route + useEffect(() => { + return () => { + setLastRoute(router.asPath); + }; + }, [router.pathname]); + + useEffect(() => { + const lang = getLangStore() || 'zh'; + i18n?.changeLanguage?.(lang); + }, [refresh, router.asPath]); + + return ( + <> + + Sealos Work Order + + + + + + + + + + + + ); +} + +export default appWithTranslation(App); diff --git a/frontend/providers/workorder/src/pages/_document.tsx b/frontend/providers/workorder/src/pages/_document.tsx new file mode 100644 index 000000000..5cb7e9e66 --- /dev/null +++ b/frontend/providers/workorder/src/pages/_document.tsx @@ -0,0 +1,21 @@ +import { Head, Html, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + + + + + + + + + +
+ + + + ); +} diff --git a/frontend/providers/workorder/src/pages/api/ai/fastgpt.ts b/frontend/providers/workorder/src/pages/api/ai/fastgpt.ts new file mode 100644 index 000000000..913a6581a --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/ai/fastgpt.ts @@ -0,0 +1,87 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { getOrderByOrderIdAndUserId } from '@/services/db/workorder'; +import type { NextApiRequest, NextApiResponse } from 'next'; +const fastgpt_url = process.env.FASTGPT_API_URL; +const fastgpt_key = process.env.FASTGPT_API_KEY; +const fastgpt_limit = parseInt(process.env.FASTGPT_API_LIMIT || '50'); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (!fastgpt_url || !fastgpt_key) { + return jsonRes(res, { code: 500, data: 'miss fastgppt key' }); + } + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const { orderId } = req.body as { orderId: string }; + const result = await getOrderByOrderIdAndUserId({ + orderId, + userId: payload.userId + }); + if (!result || result?.manualHandling.isManuallyHandled) return; + + const robotMessages = + result?.dialogs?.filter((dialog) => !dialog.isAdmin && dialog.userId === 'robot') || []; + + const userMessages = result?.dialogs + ?.filter((dialog) => !dialog.isAdmin && dialog.userId === payload.userId) + .map((item) => { + return { + content: item.content, + role: 'user' + }; + }); + + const data = { + chatId: orderId, + stream: true, + detail: false, + messages: userMessages + }; + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Content-Type', 'text/event-stream;charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache, no-transform'); + res.setHeader('X-Accel-Buffering', 'no'); + + if (robotMessages.length > fastgpt_limit) { + const message = 'If the call limit is exceeded, please transfer it to manual processing.'; + const eventStream = `event: [LIMIT]\ndata: ${message}\n\n`; + res.setHeader('Content-Length', Buffer.byteLength(eventStream, 'utf-8')); + res.write(eventStream); + res.end(); + return; + } + + const response = await fetch(fastgpt_url, { + method: 'POST', + headers: { + Authorization: `Bearer ${fastgpt_key}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const reader = response.body?.getReader(); + if (!reader) return res.end(); + + req.socket.on('close', () => { + res.end(); + }); + + while (true) { + const { value, done } = await reader.read(); + if (done) { + res.end(); + } + res.write(value); + } + } catch (error) { + res.end(); + } +} diff --git a/frontend/providers/workorder/src/pages/api/auth/desktop.ts b/frontend/providers/workorder/src/pages/api/auth/desktop.ts new file mode 100644 index 000000000..e9e57f514 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/auth/desktop.ts @@ -0,0 +1,66 @@ +import { generateAccessToken, verifyDesktopToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { createUser, getUserById, updateUser } from '@/services/db/user'; +import { AppSession } from '@/types/user'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { omit } from 'lodash'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { token } = req.body; + const payload = await verifyDesktopToken(token); + + if (!payload) { + return jsonRes(res, { + code: 401, + message: 'verify desktop token error' + }); + } + const newPayload = omit(payload, ['iat', 'exp']); + + const existingUser = await getUserById(newPayload.userId); + + if (existingUser) { + const temp = { + workspaceId: payload.workspaceId, + workspaceUid: payload.workspaceUid, + regionUid: payload.regionUid, + userCrUid: payload.userCrUid, + userCrName: payload.userCrName + }; + await updateUser(payload.userId, temp); + + const token = generateAccessToken(existingUser); + return jsonRes(res, { + code: 200, + data: { + token: token, + user: { ...existingUser, ...temp } + } + }); + } + + await createUser({ + ...newPayload, + isAdmin: false + }); + + const accessToken = generateAccessToken({ + ...newPayload, + isAdmin: false + }); + + jsonRes(res, { + code: 200, + data: { + token: accessToken, + user: { + ...newPayload, + isAdmin: false + } + } + }); + } catch (error) { + jsonRes(res, { code: 500, error: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/auth/findById.ts b/frontend/providers/workorder/src/pages/api/auth/findById.ts new file mode 100644 index 000000000..16d91582d --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/auth/findById.ts @@ -0,0 +1,42 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { getRegionById } from '@/services/db/region'; +import { getOrderByOrderIdAndUserId } from '@/services/db/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const { orderId } = req.query as { + orderId: string; + }; + + if (!payload.isAdmin) { + return jsonRes(res, { + code: 401, + message: 'unauthorized' + }); + } + + const result = await getOrderByOrderIdAndUserId({ + orderId, + userId: payload.userId + }); + const regionInfo = await getRegionById(result?.userInfo?.regionUid || ''); + + jsonRes(res, { + data: { + user: result?.userInfo, + regionInfo: regionInfo + } + }); + } catch (error) { + jsonRes(res, { code: 500, data: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/minio/delete.ts b/frontend/providers/workorder/src/pages/api/minio/delete.ts new file mode 100644 index 000000000..638f54bc4 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/minio/delete.ts @@ -0,0 +1,43 @@ +import { jsonRes } from '@/services/backend/response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { BucketName, minioClient } from './upload'; +import { verifyAccessToken } from '@/services/backend/auth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (!process.env?.MINIO_URL || !process.env?.MINIO_SECRET_KEY) { + return jsonRes(res, { + code: 500, + data: 'Missing minio service' + }); + } + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + + const { fileName } = req.query as { + fileName: string; + }; + + await minioClient.removeObject(BucketName, fileName); + + jsonRes(res, { + data: 'File deleted successfully' + }); + } catch (error) { + jsonRes(res, { + code: 500, + error + }); + } +} + +export const config = { + api: { + bodyParser: false + } +}; diff --git a/frontend/providers/workorder/src/pages/api/minio/getUrl.ts b/frontend/providers/workorder/src/pages/api/minio/getUrl.ts new file mode 100644 index 000000000..e49afbd0a --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/minio/getUrl.ts @@ -0,0 +1,43 @@ +import { jsonRes } from '@/services/backend/response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { BucketName, minioClient } from './upload'; +import { verifyAccessToken } from '@/services/backend/auth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (!process.env?.MINIO_URL || !process.env?.MINIO_SECRET_KEY) { + return jsonRes(res, { + code: 500, + data: 'Missing minio service' + }); + } + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + + const { fileName } = req.query as { + fileName: string; + }; + + const url = await minioClient.presignedGetObject(BucketName, fileName); + + jsonRes(res, { + data: url + }); + } catch (error) { + jsonRes(res, { + code: 500, + error + }); + } +} + +export const config = { + api: { + bodyParser: false + } +}; diff --git a/frontend/providers/workorder/src/pages/api/minio/upload.ts b/frontend/providers/workorder/src/pages/api/minio/upload.ts new file mode 100644 index 000000000..2bd70819b --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/minio/upload.ts @@ -0,0 +1,127 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import * as Minio from 'minio'; +import multer from 'multer'; +import { customAlphabet } from 'nanoid'; +import { NextApiRequest, NextApiResponse } from 'next'; +import path from 'path'; +const nanoid = customAlphabet('1234567890abcdef', 12); + +type FileType = { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; + filename: string; + path: string; + size: number; +}; + +/** + * Creates the multer uploader + */ +const maxSize = 5 * 1024 * 1024 * 1024; + +class UploadModel { + uploader = multer({ + limits: { + fieldSize: maxSize + }, + preservePath: true, + storage: multer.diskStorage({ + filename: (_req, file, cb) => { + const { ext } = path.parse(decodeURIComponent(file.originalname)); + cb(null, nanoid() + ext); + } + }) + }).any(); + + async doUpload(req: NextApiRequest, res: NextApiResponse) { + return new Promise<{ + files: FileType[]; + overwrite: string; + }>((resolve, reject) => { + // @ts-ignore + this.uploader(req, res, (error) => { + if (error) { + console.log(error, 'multer err'); + return reject(error); + } + resolve({ + ...req.body, + files: + // @ts-ignore + req.files?.map((file) => ({ + ...file, + originalname: decodeURIComponent(file.originalname) + })) || [] + }); + }); + }); + } +} + +const upload = new UploadModel(); + +export const minioClient = new Minio.Client({ + endPoint: process.env?.MINIO_URL || 'minioapi.dev.sealos.top', + port: Number(process.env?.MINIO_PORT) || 443, + accessKey: process.env?.MINIO_ACCESS_KEY || 'database', + secretKey: process.env?.MINIO_SECRET_KEY || 'database' +}); + +export const BucketName = process.env?.MINIO_BUCKET_NAME || 'database-test'; + +export default async function handler(req: any, res: NextApiResponse) { + try { + if (!process.env?.MINIO_URL || !process.env?.MINIO_SECRET_KEY) { + return jsonRes(res, { + code: 500, + data: 'Missing minio service' + }); + } + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const { files, overwrite = 'true' } = await upload.doUpload(req, res); + + const startTime = performance.now(); + const upLoadResults = await Promise.all( + files.map(async (file) => { + let fileName = JSON.parse(overwrite) + ? `${payload.userId}-${Date.now()}-${file.filename}` + : file.originalname; + const originalFileName = file.originalname; + + await minioClient.fPutObject(BucketName, fileName, file.path); + return { + originalName: originalFileName, + fileName: fileName + }; + }) + ); + const endTime = performance.now(); + const duration = endTime - startTime; + + console.log(upLoadResults, `time:${duration} ms`); + + jsonRes(res, { + data: upLoadResults + }); + } catch (error) { + jsonRes(res, { + code: 500, + error + }); + } +} + +export const config = { + api: { + bodyParser: false + } +}; diff --git a/frontend/providers/workorder/src/pages/api/platform/feishu.ts b/frontend/providers/workorder/src/pages/api/platform/feishu.ts new file mode 100644 index 000000000..91a23c28b --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/platform/feishu.ts @@ -0,0 +1,107 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { updateOrder } from '@/services/db/workorder'; +import { ApiResp } from '@/services/kubernet'; +import { WorkOrderType } from '@/types/workorder'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export type FeishuNotificationParams = { + type: WorkOrderType; + description: string; + orderId: string; + switchToManual?: boolean; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { + type, + description, + orderId, + switchToManual = false + } = req.body as FeishuNotificationParams; + + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const feishuUrl = process.env.ADMIN_FEISHU_URL; + const feishuCallBackUrl = process.env.ADMIN_FEISHU_CALLBACK_URL; + const title = switchToManual ? `工单:${orderId},请求人工处理` : '有新的工单,请立即查看'; + + if (switchToManual) { + await updateOrder({ + orderId, + userId: payload.userId, + updates: { + manualHandling: { isManuallyHandled: true } + } + }); + } + + const form = { + msg_type: 'interactive', + card: { + elements: [ + { + tag: 'markdown', + content: `**用户ID:** ${payload.userId}\n**可用区ID:** ${payload.regionUid}\n所属分类: ${type}\n描述信息: ${description}` + }, + { + tag: 'action', + actions: [ + { + tag: 'button', + text: { + tag: 'plain_text', + content: '查看详情' + }, + type: 'primary', + multi_url: { + url: feishuCallBackUrl + `?orderId=${orderId}`, + android_url: '', + ios_url: '', + pc_url: '' + } + } + ] + } + ], + header: { + template: 'blue', + title: { + content: title, + tag: 'plain_text' + } + } + } + }; + + if (!feishuUrl) { + return jsonRes(res, { + code: 500, + message: 'Missing Feishu API' + }); + } + const data = await fetch(feishuUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(form) + }); + const result = await data.json(); + jsonRes(res, { + data: result + }); + } catch (error) { + console.log(error); + jsonRes(res, { + code: 500, + error + }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/platform/getEnv.ts b/frontend/providers/workorder/src/pages/api/platform/getEnv.ts new file mode 100644 index 000000000..24dee8c5e --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/platform/getEnv.ts @@ -0,0 +1,15 @@ +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export type SystemEnvResponse = { + domain?: string; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + jsonRes(res, { + data: { + domain: process.env.SEALOS_DOMAIN || 'cloud.sealos.io' + } + }); +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/create.ts b/frontend/providers/workorder/src/pages/api/workorder/create.ts new file mode 100644 index 000000000..c4b565991 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/create.ts @@ -0,0 +1,52 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { createOrder } from '@/services/db/workorder'; +import { WorkOrderDB, WorkOrderStatus, WorkOrderType } from '@/types/workorder'; +import { customAlphabet } from 'nanoid'; +import { NextApiRequest, NextApiResponse } from 'next'; +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12); + +export type CreateWorkOrderParams = { + type: WorkOrderType; + description: string; + appendix?: string[]; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { type, description, appendix } = req.body as CreateWorkOrderParams; + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + + const workorder: WorkOrderDB = { + userId: payload.userId, + orderId: nanoid(), + type, + updateTime: new Date(), + createTime: new Date(), + description, + status: WorkOrderStatus.Pending, + appendix, + manualHandling: { + isManuallyHandled: false + }, + userInfo: payload + }; + + await createOrder({ order: workorder }); + + jsonRes(res, { + data: { + orderId: workorder.orderId + } + }); + } catch (error) { + console.log(error); + jsonRes(res, { code: 500, data: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/delete.ts b/frontend/providers/workorder/src/pages/api/workorder/delete.ts new file mode 100644 index 000000000..25d37d8f7 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/delete.ts @@ -0,0 +1,31 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { deleteOrder } from '@/services/db/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + + const { orderId } = req.query as { + orderId: string; + }; + + const result = await deleteOrder({ + orderId: orderId, + userId: payload.userId + }); + + jsonRes(res, { + data: result + }); + } catch (error) { + jsonRes(res, { code: 500, data: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/getByID.ts b/frontend/providers/workorder/src/pages/api/workorder/getByID.ts new file mode 100644 index 000000000..7422acc51 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/getByID.ts @@ -0,0 +1,36 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { getOrderByOrderIdAndUserId } from '@/services/db/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const { orderId } = req.query as { + orderId: string; + }; + + const result = await getOrderByOrderIdAndUserId({ + orderId, + userId: payload.userId + }); + + if (!result) { + return jsonRes(res, { + code: 404, + message: 'Not Found' + }); + } + jsonRes(res, { + data: result + }); + } catch (error) { + jsonRes(res, { code: 500, data: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/listByUser.ts b/frontend/providers/workorder/src/pages/api/workorder/listByUser.ts new file mode 100644 index 000000000..9fbb7fdd4 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/listByUser.ts @@ -0,0 +1,50 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { getAllOrdersByUserId } from '@/services/db/workorder'; +import { WorkOrderStatus, WorkOrderType } from '@/types/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { + page = 1, + pageSize = 10, + orderStatus, + orderType, + startTime, + endTime + } = req.body as { + page: number; + pageSize: number; + orderType?: WorkOrderType; + orderStatus?: WorkOrderStatus; + startTime?: Date; + endTime?: Date; + }; + + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + + let result = await getAllOrdersByUserId({ + userId: payload.userId, + page: page, + pageSize: pageSize, + orderStatus, + orderType, + startTime, + endTime + }); + + jsonRes(res, { + data: result + }); + } catch (error) { + console.log(error); + jsonRes(res, { code: 500, error: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/update.ts b/frontend/providers/workorder/src/pages/api/workorder/update.ts new file mode 100644 index 000000000..08e233414 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/update.ts @@ -0,0 +1,33 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { updateOrder } from '@/services/db/workorder'; +import { WorkOrderDB } from '@/types/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const { orderId, updates } = req.body as { + updates: Partial; + orderId: string; + }; + + const result = await updateOrder({ + orderId, + userId: payload.userId, + updates + }); + + jsonRes(res, { + data: result + }); + } catch (error) { + jsonRes(res, { code: 500, data: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts b/frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts new file mode 100644 index 000000000..a44906c19 --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/updateDialog.ts @@ -0,0 +1,56 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { addDialogToOrder, updateOrder } from '@/services/db/workorder'; +import { WorkOrderStatus } from '@/types/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = await verifyAccessToken(req); + if (!payload) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const { + orderId, + content, + isRobot = false + } = req.body as { + orderId: string; + content: string; + isRobot: boolean; + }; + + // If the admin replies, it is being processed. + if (payload.isAdmin) { + await updateOrder({ + orderId, + userId: payload.userId, + updates: { + status: WorkOrderStatus.Processing, + manualHandling: { isManuallyHandled: true, handlingTime: new Date() } + } + }); + } + + const result = await addDialogToOrder({ + orderId, + userId: payload.userId, + dialog: { + userId: isRobot ? 'robot' : payload.userId, + time: new Date(), + content: content, + isAdmin: payload.isAdmin, + isAIBot: isRobot + } + }); + + jsonRes(res, { + data: result + }); + } catch (error) { + jsonRes(res, { code: 500, data: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/index.tsx b/frontend/providers/workorder/src/pages/index.tsx new file mode 100644 index 000000000..4531d85e3 --- /dev/null +++ b/frontend/providers/workorder/src/pages/index.tsx @@ -0,0 +1,22 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +export default function Index() { + const router = useRouter(); + + useEffect(() => { + if (router?.query?.orderId) { + const orderId = router?.query?.orderId; + router.push({ + pathname: '/workorder/detail', + query: { + orderId: orderId + } + }); + } else { + router.push('/workorders'); + } + }, [router]); + + return
; +} diff --git a/frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx b/frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx new file mode 100644 index 000000000..707bf7ced --- /dev/null +++ b/frontend/providers/workorder/src/pages/workorder/create/components/ErrorModal.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + Box +} from '@chakra-ui/react'; +import MyIcon from '@/components/Icon'; + +const ErrorModal = ({ + title, + content, + onClose +}: { + title: string; + content: string; + onClose: () => void; +}) => { + return ( + + + + + + + {title} + + + + + {content} + + + + ); +}; + +export default ErrorModal; diff --git a/frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx b/frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx new file mode 100644 index 000000000..b46528212 --- /dev/null +++ b/frontend/providers/workorder/src/pages/workorder/create/components/Header.tsx @@ -0,0 +1,44 @@ +import MyIcon from '@/components/Icon'; +import { useGlobalStore } from '@/store/global'; +import { Box, Button, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; + +const Header = ({ + title, + applyCb, + applyBtnText +}: { + title: string; + applyCb: () => void; + applyBtnText: string; +}) => { + const { t } = useTranslation(); + const router = useRouter(); + const { lastRoute } = useGlobalStore(); + + return ( + + router.replace(lastRoute)}> + + + {t(title)} + + + + + + ); +}; + +export default Header; diff --git a/frontend/providers/workorder/src/pages/workorder/create/index.tsx b/frontend/providers/workorder/src/pages/workorder/create/index.tsx new file mode 100644 index 000000000..283d1aaf6 --- /dev/null +++ b/frontend/providers/workorder/src/pages/workorder/create/index.tsx @@ -0,0 +1,214 @@ +import { createWorkOrder, updateWorkOrderDialogById } from '@/api/workorder'; +import FileSelect, { FileItemType } from '@/components/FileSelect'; +import MyIcon from '@/components/Icon'; +import MySelect from '@/components/Select'; +import { OrderTypeList } from '@/constants/workorder'; +import { useConfirm } from '@/hooks/useConfirm'; +import { useLoading } from '@/hooks/useLoading'; +import { useToast } from '@/hooks/useToast'; +import { useGlobalStore } from '@/store/global'; +import { WorkOrderEditForm, WorkOrderType } from '@/types/workorder'; +import { serviceSideProps } from '@/utils/i18n'; +import { Box, BoxProps, Flex, Icon, Text, Textarea } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import ErrorModal from './components/ErrorModal'; +import Header from './components/Header'; +import { FeishuNotification, uploadFile } from '@/api/platform'; + +export default function EditOrder() { + const [errorMessage, setErrorMessage] = useState(''); + const [files, setFiles] = useState([]); + const { t } = useTranslation(); + const { toast } = useToast(); + const { Loading, setIsLoading } = useLoading(); + const router = useRouter(); + const { screenWidth, lastRoute } = useGlobalStore(); + const [forceUpdate, setForceUpdate] = useState(false); + const [fileProgressText, setFileProgressText] = useState(''); + + const { openConfirm, ConfirmChild } = useConfirm({ + content: t('Are you sure you want to submit this work order') + }); + + // form + const formHook = useForm({ + defaultValues: { + type: WorkOrderType.AppLaunchpad, + description: '' + } + }); + + // watch form change, compute new yaml + formHook.watch((data) => { + setForceUpdate(!forceUpdate); + }); + + const submitSuccess = async (data: WorkOrderEditForm) => { + setIsLoading(true); + try { + const form = new FormData(); + files.forEach((item) => { + form.append('file', item.file, encodeURIComponent(item.filename)); + }); + form.append('overwrite', 'false'); + const result = await uploadFile(form); + const res = await createWorkOrder({ + type: data.type, + description: data.description, + appendix: result.map((i) => i.fileName) + }); + await updateWorkOrderDialogById({ + orderId: res.orderId, + content: data.description + }); + await FeishuNotification({ + type: data.type, + description: data.description, + orderId: res.orderId + }); + toast({ + status: 'success', + title: 'success' + }); + router.push(`/workorder/detail?orderId=${res.orderId}`); + } catch (error) { + console.error(error); + setErrorMessage(JSON.stringify(error)); + } + setIsLoading(false); + }; + + const submitError = useCallback(() => { + // deep search message + const deepSearch = (obj: any): string => { + if (!obj) return t('Submit Error'); + if (!!obj.message) { + return obj.message; + } + return deepSearch(Object.values(obj)[0]); + }; + + toast({ + title: deepSearch(formHook.formState.errors), + status: 'error', + position: 'top', + duration: 3000, + isClosable: true + }); + }, [formHook.formState.errors, t, toast]); + + const Label = ({ + children, + w = 'auto', + LabelStyle + }: { + children: string; + w?: number | 'auto'; + LabelStyle?: BoxProps; + }) => ( + + {children} + + ); + + return ( + +
+ formHook.handleSubmit((data) => openConfirm(() => submitSuccess(data))(), submitError)() + } + applyBtnText="Submit Order" + /> + + + + + + {t('Problem Description')} + + + + + + { + formHook.setValue('type', val); + }} + /> + + + + + + + +