diff --git a/frontend/plugins/kubepanel/deploy/Kubefile b/frontend/plugins/kubepanel/deploy/Kubefile new file mode 100644 index 00000000000..3ddf209baae --- /dev/null +++ b/frontend/plugins/kubepanel/deploy/Kubefile @@ -0,0 +1,10 @@ +FROM scratch + +USER 65532:65532 + +COPY manifests manifests + +ENV cloudDomain="127.0.0.1.nip.io" +ENV cloudPort="" + +CMD ["kubectl apply -f manifests"] \ No newline at end of file diff --git a/frontend/plugins/kubepanel/deploy/menifests/appcr.yaml.tmpl b/frontend/plugins/kubepanel/deploy/menifests/appcr.yaml.tmpl new file mode 100644 index 00000000000..920a3eabb41 --- /dev/null +++ b/frontend/plugins/kubepanel/deploy/menifests/appcr.yaml.tmpl @@ -0,0 +1,21 @@ +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: kubepanel + namespace: app-system +spec: + data: + desc: Kube Panel + url: "https://kubepanel.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}" + icon: "https://kubepanel.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}/logo.svg" + i18n: + zh: + name: 资源监控 + zh-Hans: + name: 资源监控 + menuData: + helpDropDown: false + nameColor: text-black + name: Kube Panel + type: iframe + displayType: normal diff --git a/frontend/plugins/kubepanel/deploy/menifests/deploy.yaml.tmpl b/frontend/plugins/kubepanel/deploy/menifests/deploy.yaml.tmpl new file mode 100644 index 00000000000..5559fa15364 --- /dev/null +++ b/frontend/plugins/kubepanel/deploy/menifests/deploy.yaml.tmpl @@ -0,0 +1,85 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app: kubepanel-frontend + name: kubepanel-frontend +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: kubepanel-frontend-config + namespace: kubepanel-frontend +data: + config.yaml: |- + addr: :3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kubepanel-frontend + namespace: kubepanel-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: kubepanel-frontend + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% + template: + metadata: + labels: + app: kubepanel-frontend + spec: + serviceAccountName: cluster-version-reader + containers: + - name: kubepanel-frontend + env: + - name: SEALOS_DOMAIN + value: {{ .cloudDomain }} + - name: SEALOS_PORT + value: "{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}" + 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-kubepanel-frontend:latest + imagePullPolicy: Always + volumeMounts: + - name: kubepanel-frontend-volume + mountPath: /config.yaml + subPath: config.yaml + volumes: + - name: kubepanel-frontend-volume + configMap: + name: kubepanel-frontend-config +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: kubepanel-frontend + name: kubepanel-frontend + namespace: kubepanel-frontend +spec: + ports: + - name: http + port: 3000 + protocol: TCP + targetPort: 3000 + selector: + app: kubepanel-frontend diff --git a/frontend/plugins/kubepanel/deploy/menifests/ingress.yaml.tmpl b/frontend/plugins/kubepanel/deploy/menifests/ingress.yaml.tmpl new file mode 100644 index 00000000000..00390b7992f --- /dev/null +++ b/frontend/plugins/kubepanel/deploy/menifests/ingress.yaml.tmpl @@ -0,0 +1,45 @@ +# Copyright © 2022 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/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; frame-src 'self' *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} mailto: tel: weixin: mtt: *.baidu.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; frame-src 'self' *.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} {{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} mailto: tel: weixin: mtt: *.baidu.com; frame-ancestors 'self' https://{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }} https://*.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}" + X-Xss-Protection "1; mode=block" + name: kubepanel-frontend + namespace: kubepanel-frontend +spec: + rules: + - host: kubepanel.{{ .cloudDomain }} + http: + paths: + - pathType: Prefix + path: / + backend: + service: + name: kubepanel-frontend + port: + number: 3000 + tls: + - hosts: + - kubepanel.{{ .cloudDomain }} + secretName: {{ .certSecretName }} diff --git a/frontend/plugins/kubepanel/package.json b/frontend/plugins/kubepanel/package.json index 96a015f1fd9..ca16815a9a5 100644 --- a/frontend/plugins/kubepanel/package.json +++ b/frontend/plugins/kubepanel/package.json @@ -1,14 +1,14 @@ { "name": "kubepanel", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", - "link-sdk": "rm -rf node_modules/sealos-desktop-sdk && yalc link sealos-desktop-sdk", - "unlink-sdk": "yalc remove --all && pnpm install sealos-desktop-sdk" + "link-sdk": "pnpm link sealos-desktop-sdk", + "unlink-sdk": "pnpm unlink sealos-desktop-sdk" }, "dependencies": { "@ant-design/charts": "^1.4.2", @@ -21,10 +21,11 @@ "@emotion/styled": "^11.11.0", "@kubernetes/client-node": "^0.19.0", "@monaco-editor/react": "^4.6.0", - "@tanstack/react-query": "^4.29.25", "antd": "^5.11.0", "auto-bind": "^5.0.1", "axios": "^1.5.1", + "byline": "^5.0.0", + "eventsource": "^2.0.2", "framer-motion": "^10.16.4", "fs": "0.0.1-security", "immer": "^10.0.3", @@ -47,6 +48,8 @@ "zustand-computed": "^1.3.7" }, "devDependencies": { + "@types/byline": "^4.2.36", + "@types/eventsource": "^1.1.15", "@types/js-yaml": "^4.0.8", "@types/lodash": "^4.14.199", "@types/node": "^20", diff --git a/frontend/plugins/kubepanel/pnpm-lock.yaml b/frontend/plugins/kubepanel/pnpm-lock.yaml index 9e38c65031f..2070e75824b 100644 --- a/frontend/plugins/kubepanel/pnpm-lock.yaml +++ b/frontend/plugins/kubepanel/pnpm-lock.yaml @@ -38,9 +38,6 @@ importers: '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.44.0)(react-dom@18.2.0)(react@18.2.0) - '@tanstack/react-query': - specifier: ^4.29.25 - version: 4.29.25(react-dom@18.2.0)(react@18.2.0) antd: specifier: ^5.11.0 version: 5.11.0(moment@2.29.4)(react-dom@18.2.0)(react@18.2.0) @@ -50,6 +47,12 @@ importers: axios: specifier: ^1.5.1 version: 1.5.1 + byline: + specifier: ^5.0.0 + version: 5.0.0 + eventsource: + specifier: ^2.0.2 + version: 2.0.2 framer-motion: specifier: ^10.16.4 version: 10.16.4(react-dom@18.2.0)(react@18.2.0) @@ -111,6 +114,12 @@ importers: specifier: ^1.3.7 version: 1.3.7(react@18.2.0)(zustand@4.3.9) devDependencies: + '@types/byline': + specifier: ^4.2.36 + version: 4.2.36 + '@types/eventsource': + specifier: ^1.1.15 + version: 1.1.15 '@types/js-yaml': specifier: ^4.0.8 version: 4.0.8 @@ -2890,28 +2899,6 @@ packages: tslib: 2.6.2 dev: false - /@tanstack/query-core@4.29.25: - resolution: {integrity: sha512-DI4y4VC6Uw4wlTpOocEXDky69xeOScME1ezLKsj+hOk7DguC9fkqXtp6Hn39BVb9y0b5IBrY67q6kIX623Zj4Q==} - dev: false - - /@tanstack/react-query@4.29.25(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-c1+Ezu+XboYrdAMdusK2fTdRqXPMgPAnyoTrzHOZQqr8Hqz6PNvV9DSKl8agUo6nXX4np7fdWabIprt+838dLg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - dependencies: - '@tanstack/query-core': 4.29.25 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - use-sync-external-store: 1.2.0(react@18.2.0) - dev: false - /@turf/bbox-polygon@6.5.0: resolution: {integrity: sha512-+/r0NyL1lOG3zKZmmf6L8ommU07HliP4dgYToMoTxqzsWzyLjaj/OzgQ8rBmv703WJX+aS6yCmLuIhYqyufyuw==} dependencies: @@ -2966,6 +2953,12 @@ packages: resolution: {integrity: sha512-0JNr89BJO6eqw8o9KMXqy+3KtpqNvyhIyobiWQ2uM0E27jRWmEvE1b6GKyQr38imkYBWhjaMt/p9W+w+/uoaow==} dev: false + /@types/byline@4.2.36: + resolution: {integrity: sha512-dO55KDSaOSE+3T8TwP66mzn0u/PM/aSedVMr1tby7WBNjfLIuS6IbYXi1mlau49sVSVB+gXKJscWE0JO3tlXDw==} + dependencies: + '@types/node': 20.8.7 + dev: true + /@types/caseless@0.12.4: resolution: {integrity: sha512-2in/lrHRNmDvHPgyormtEralhPcN3An1gLjJzj2Bw145VBxkQ75JEXW6CTdMAwShiHQcYsl2d10IjQSdJSJz4g==} @@ -2977,6 +2970,10 @@ packages: resolution: {integrity: sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==} dev: true + /@types/eventsource@1.1.15: + resolution: {integrity: sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==} + dev: true + /@types/fs-extra@8.1.4: resolution: {integrity: sha512-OMcQKnlrkrOI0TaZ/MgVDA8LYFl7CykzFsjMj9l5x3un2nFxCY20ZFlnqrM0lcqlbs0Yro2HbnZlmopyRaoJ5w==} dependencies: @@ -4489,6 +4486,11 @@ packages: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false + /eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + dev: false + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} diff --git a/frontend/plugins/kubepanel/public/favicon.ico b/frontend/plugins/kubepanel/public/favicon.ico index 3dcc72fb482..eba27e52d37 100644 Binary files a/frontend/plugins/kubepanel/public/favicon.ico and b/frontend/plugins/kubepanel/public/favicon.ico differ diff --git a/frontend/plugins/kubepanel/public/logo.svg b/frontend/plugins/kubepanel/public/logo.svg new file mode 100644 index 00000000000..6a6bb3dd0ae --- /dev/null +++ b/frontend/plugins/kubepanel/public/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/plugins/kubepanel/src/api/create.ts b/frontend/plugins/kubepanel/src/api/create.ts deleted file mode 100644 index b1ec25e1664..00000000000 --- a/frontend/plugins/kubepanel/src/api/create.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Resources } from '@/constants/kube-object'; -import { POST } from '@/services/request'; - -interface Response { - code: number; - data: { - message: string; - }; -} - -export const createResource = (data: string, resource: Resources) => - POST(`/api/create?resource=${resource}`, { - data - }); diff --git a/frontend/plugins/kubepanel/src/api/delete.ts b/frontend/plugins/kubepanel/src/api/delete.ts deleted file mode 100644 index 94f2347d4c6..00000000000 --- a/frontend/plugins/kubepanel/src/api/delete.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Resources } from '@/constants/kube-object'; -import { ApiResp } from '@/services/kubernet'; -import { DELETE } from '@/services/request'; - -export const deleteResource = (name: string, resource: Resources) => - DELETE(`/api/delete?resource=${resource}&name=${name}`); diff --git a/frontend/plugins/kubepanel/src/api/kubernetes.ts b/frontend/plugins/kubepanel/src/api/kubernetes.ts new file mode 100644 index 00000000000..eb65ab08ca9 --- /dev/null +++ b/frontend/plugins/kubepanel/src/api/kubernetes.ts @@ -0,0 +1,112 @@ +import { + KubeJsonApiDataFor, + KubeObject, + isJsonApiData, + isPartialJsonApiData +} from '@/k8slens/kube-object'; +import { bindPredicate, hasTypedProperty, isTypedArray } from '@/k8slens/utilities'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { DELETE, GET, POST, PUT } from '@/services/request'; +import { KubeList } from '@/types/kube-resource'; +import { isKubeList } from '@/utils/types'; + +/** + * Retrieves a yaml-like template of a specified kind from Backend API. + * + * @param kind The kind of resource to retrieve. + * @returns A promise that resolves to a `SuccessResponse` containing the yaml-like template. + */ +export function getTemplate(kind: string) { + return GET>(`/api/kubernetes/template?kind=${kind}`); +} + +/** + * Retrieves a list of resources of a specified kind from the Backend API. + * + * @param kind The kind of resource to retrieve. + * @returns A promise that resolves to a `SuccessResponse` containing the list of resources. + */ +export async function listResources< + Object extends KubeObject = KubeObject, + Data extends KubeJsonApiDataFor = KubeJsonApiDataFor +>(kind: string): Promise>> { + const res = await GET>>(`/api/kubernetes/list?kind=${kind}`); + + if ( + isKubeList(res.data) && + hasTypedProperty(res.data, 'items', bindPredicate(isTypedArray, isPartialJsonApiData)) + ) { + return res; + } + + throw buildErrno('Response is not correct type', ErrnoCode.ServerInternalError); +} + +/** + * Creates a resource of the specified kind using the provided data. + * + * @param kind The kind of resource to create. + * @param data The data to use for creating the resource. + * @returns A Promise that resolves to a `SuccessResponse` containing the created resource data. + */ +export async function createResource< + Object extends KubeObject = KubeObject, + Data extends KubeJsonApiDataFor = KubeJsonApiDataFor +>(kind: string, data: string): Promise> { + const res = await POST>(`/api/kubernetes/create?kind=${kind}`, { + data + }); + + if (isJsonApiData(res.data)) { + return res; + } + + throw buildErrno('Response is not correct type', ErrnoCode.ServerInternalError); +} + +/** + * Deletes a resource from the Kubernetes cluster. + * + * @param kind The kind of resource to delete. + * @param name The name of the resource to delete. + * @return A promise that resolves with a `SuccessResponse` containing the data, which is old resource data, + * if the resource is deleted successfully. + */ +export async function deleteResource< + Object extends KubeObject = KubeObject, + Data extends KubeJsonApiDataFor = KubeJsonApiDataFor +>(kind: string, name: string): Promise> { + const res = await DELETE>('/api/kubernetes/delete', { + kind, + name + }); + + if (isJsonApiData(res.data)) { + return res; + } + + throw buildErrno('Response is not correct type', ErrnoCode.ServerInternalError); +} + +/** + * Updates a resource in the Kubernetes API. + * + * @param kind The type of resource to update. + * @param name The name of the resource to update. + * @param data The updated data for the resource. + * @return A promise that resolves to a `SuccessResponse` containing the old data. + */ +export async function updateResource< + Object extends KubeObject = KubeObject, + Data extends KubeJsonApiDataFor = KubeJsonApiDataFor +>(kind: string, name: string, data: string): Promise> { + const res = await PUT>(`/api/kubernetes/update?kind=${kind}&name=${name}`, { + data + }); + + if (isJsonApiData(res.data)) { + return res; + } + + throw buildErrno('Response is not correct type', ErrnoCode.ServerInternalError); +} diff --git a/frontend/plugins/kubepanel/src/api/list.ts b/frontend/plugins/kubepanel/src/api/list.ts deleted file mode 100644 index b5f56c6ff6b..00000000000 --- a/frontend/plugins/kubepanel/src/api/list.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { KubeObjectConstructorMap, Resources } from '@/constants/kube-object'; -import { - KubeJsonApiDataFor, - KubeObject, - KubeObjectConstructor, - isJsonApiDataList, - isPartialJsonApiData -} from '@/k8slens/kube-object'; -import { isDefined } from '@/k8slens/utilities'; -import { GET } from '@/services/request'; -import { isArray } from 'lodash'; - -export const getResource = async ( - resource: Resources -): Promise => { - try { - const res = await GET(`/api/list?resource=${resource}`); - - const parsed = parseResponse(res, KubeObjectConstructorMap[resource]); - - if (isArray(parsed)) { - return parsed; - } - - if (!parsed) return []; - - return Promise.reject( - new Error( - `GET multiple request to ${resource} returned not an array: ${JSON.stringify(parsed)}` - ) - ); - } catch (err) { - return Promise.reject(err); - } -}; - -const parseResponse = < - Object extends KubeObject = KubeObject, - Data extends KubeJsonApiDataFor = KubeJsonApiDataFor ->( - data: unknown, - objectConstructor: KubeObjectConstructor -): Object[] | null => { - if (!data) { - return null; - } - - const KubeObjectConstructor = objectConstructor; - - if (isJsonApiDataList(data, isPartialJsonApiData)) { - const { apiVersion, items } = data; - - return items - .map((item) => { - if (!item.metadata) { - return undefined; - } - - const object = new KubeObjectConstructor({ - ...(item as Data), - kind: objectConstructor.kind, - apiVersion - }); - return object; - }) - .filter(isDefined); - } - - return null; -}; diff --git a/frontend/plugins/kubepanel/src/api/template.ts b/frontend/plugins/kubepanel/src/api/template.ts deleted file mode 100644 index 6485b006222..00000000000 --- a/frontend/plugins/kubepanel/src/api/template.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { KindMap, Resources } from '@/constants/kube-object'; -import { GET } from '@/services/request'; - -export const getTemplate = (resource: Resources) => - GET(`/api/template?name=${KindMap[resource]}`); diff --git a/frontend/plugins/kubepanel/src/api/update.ts b/frontend/plugins/kubepanel/src/api/update.ts deleted file mode 100644 index 24d68d2b811..00000000000 --- a/frontend/plugins/kubepanel/src/api/update.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Resources } from '@/constants/kube-object'; -import { ApiResp } from '@/services/kubernet'; -import { PUT } from '@/services/request'; - -export const updateResource = (data: string, name: string, resource: Resources) => - PUT(`/api/update?resource=${resource}&name=${name}`, { - data - }); diff --git a/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx b/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx index 3b361cc79ec..7378d4835c2 100644 --- a/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx +++ b/frontend/plugins/kubepanel/src/components/kube/kube-badge.tsx @@ -24,7 +24,6 @@ export const KubeBadge = ({ }, [expandable]); const onClick = (e: React.MouseEvent) => { - console.log(e.target); if (isExpandable) { setIsExpanded(!isExpanded); } diff --git a/frontend/plugins/kubepanel/src/constants/kube-api.ts b/frontend/plugins/kubepanel/src/constants/kube-api.ts deleted file mode 100644 index ffad20f76f2..00000000000 --- a/frontend/plugins/kubepanel/src/constants/kube-api.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { KubeApiUrlParams } from '@/services/backend/api'; -import { Resources, ResourceKey } from './kube-object'; - -type UrlParams = Omit; - -export const ApiBaseParamsMap: Record = { - pods: { - apiPrefix: 'api', - apiGroup: undefined, - apiVersion: 'v1', - resource: Resources.Pods - }, - deployments: { - apiPrefix: 'apis', - apiGroup: 'apps', - apiVersion: 'v1', - resource: Resources.Deployments - }, - statefulsets: { - apiPrefix: 'apis', - apiGroup: 'apps', - apiVersion: 'v1', - resource: Resources.StatefulSets - }, - configmaps: { - apiPrefix: 'api', - apiGroup: undefined, - apiVersion: 'v1', - resource: Resources.ConfigMaps - }, - persistentvolumeclaims: { - apiPrefix: 'api', - apiGroup: undefined, - apiVersion: 'v1', - resource: Resources.PersistentVolumeClaims - }, - secrets: { - apiPrefix: 'api', - apiGroup: undefined, - apiVersion: 'v1', - resource: Resources.Secrets - }, - ingresses: { - apiPrefix: 'apis', - apiGroup: 'networking.k8s.io', - apiVersion: 'v1', - resource: Resources.Ingresses - }, - events: { - apiPrefix: 'api', - apiGroup: undefined, - apiVersion: 'v1', - resource: Resources.Events - } -}; diff --git a/frontend/plugins/kubepanel/src/constants/kube-object.ts b/frontend/plugins/kubepanel/src/constants/kube-object.ts index 10eda5001d6..1ac6a1ed46a 100644 --- a/frontend/plugins/kubepanel/src/constants/kube-object.ts +++ b/frontend/plugins/kubepanel/src/constants/kube-object.ts @@ -1,46 +1,10 @@ -import { - ConfigMap, - Deployment, - Ingress, - KubeEvent, - PersistentVolumeClaim, - Pod, - Secret, - StatefulSet -} from '@/k8slens/kube-object'; -import { StringKeyOf } from 'type-fest'; - -export type ResourceKey = Lowercase>; - -export enum Resources { - Pods = 'pods', - Deployments = 'deployments', - StatefulSets = 'statefulsets', - ConfigMaps = 'configmaps', - PersistentVolumeClaims = 'persistentvolumeclaims', - Secrets = 'secrets', - Ingresses = 'ingresses', - Events = 'events' +export enum KubeObjectKind { + Pod = 'Pod', + Deployment = 'Deployment', + StatefulSet = 'StatefulSet', + ConfigMap = 'ConfigMap', + PersistentVolumeClaim = 'PersistentVolumeClaim', + Secret = 'Secret', + Ingress = 'Ingress', + Event = 'Event' } - -export const KubeObjectConstructorMap: { [key in Resources]: any } = { - [Resources.Pods]: Pod, - [Resources.Deployments]: Deployment, - [Resources.StatefulSets]: StatefulSet, - [Resources.ConfigMaps]: ConfigMap, - [Resources.PersistentVolumeClaims]: PersistentVolumeClaim, - [Resources.Secrets]: Secret, - [Resources.Ingresses]: Ingress, - [Resources.Events]: KubeEvent -}; - -export const KindMap: { [key in Resources]: any } = { - [Resources.Pods]: 'Pod', - [Resources.Deployments]: 'Deployment', - [Resources.StatefulSets]: 'StatefulSet', - [Resources.ConfigMaps]: 'ConfigMap', - [Resources.PersistentVolumeClaims]: 'PersistentVolumeClaim', - [Resources.Secrets]: 'Secret', - [Resources.Ingresses]: 'Ingress', - [Resources.Events]: 'Event' -}; diff --git a/frontend/plugins/kubepanel/src/pages/_app.tsx b/frontend/plugins/kubepanel/src/pages/_app.tsx index 563253aeee4..11cc01e664f 100644 --- a/frontend/plugins/kubepanel/src/pages/_app.tsx +++ b/frontend/plugins/kubepanel/src/pages/_app.tsx @@ -1,7 +1,6 @@ import type { AppProps } from 'next/app'; import { Router, useRouter } from 'next/router'; import NProgress from 'nprogress'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useGlobalStore } from '@/store/global'; import { useLoading } from '@/hooks/useLoading'; import { useEffect } from 'react'; @@ -20,17 +19,6 @@ 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 - } - } -}); - export default function App({ Component, pageProps }: AppProps) { const router = useRouter(); const { setScreenWidth, loading, setLastRoute } = useGlobalStore(); @@ -99,11 +87,9 @@ export default function App({ Component, pageProps }: AppProps) { - - - - - + + + ); } diff --git a/frontend/plugins/kubepanel/src/pages/api/create.ts b/frontend/plugins/kubepanel/src/pages/api/create.ts deleted file mode 100644 index 7a39f4238ae..00000000000 --- a/frontend/plugins/kubepanel/src/pages/api/create.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ApiBaseParamsMap } from '@/constants/kube-api'; -import { ResourceKey } from '@/constants/kube-object'; -import { createResource } from '@/services/backend/api'; -import { authSession } from '@/services/backend/auth'; -import { getKubeApiParams } from '@/services/backend/kubernetes'; -import { jsonRes } from '@/services/backend/response'; -import { mustGetTypedProperty } from '@/utils/api'; -import yaml from 'js-yaml'; -import { isString } from 'lodash'; -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'POST') throw new Error(`Method not allowed: ${req.method}`); - - const resource = mustGetTypedProperty(req.query, 'resource', isString, 'string'); - - const apiBaseParams = ApiBaseParamsMap[resource as ResourceKey]; - if (!apiBaseParams) throw new Error(`invalid resource ${resource}`); - - const { serverUrl, requestOpts, namespace } = getKubeApiParams(await authSession(req.headers)); - - const resourceData = yaml.load(mustGetTypedProperty(req.body, 'data', isString, 'string')); - - const data = await createResource( - { - urlParams: { - ...apiBaseParams, - serverUrl, - namespace - }, - opts: requestOpts - }, - resourceData - ); - - jsonRes(res, { - code: 200, - data - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/plugins/kubepanel/src/pages/api/delete.ts b/frontend/plugins/kubepanel/src/pages/api/delete.ts deleted file mode 100644 index a9e2779d68e..00000000000 --- a/frontend/plugins/kubepanel/src/pages/api/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiBaseParamsMap } from '@/constants/kube-api'; -import { ResourceKey } from '@/constants/kube-object'; -import { deleteResource } from '@/services/backend/api'; -import { authSession } from '@/services/backend/auth'; -import { getKubeApiParams } from '@/services/backend/kubernetes'; -import { jsonRes } from '@/services/backend/response'; -import { mustGetTypedProperty } from '@/utils/api'; -import { isString } from 'lodash'; -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'DELETE') throw new Error(`Method not allowed: ${req.method}`); - - const resource = mustGetTypedProperty(req.query, 'resource', isString, 'string'); - const name = mustGetTypedProperty(req.query, 'name', isString, 'string'); - - const apiBaseParams = ApiBaseParamsMap[resource as ResourceKey]; - if (!apiBaseParams) throw new Error(`invalid resource ${resource}`); - - const { serverUrl, requestOpts, namespace } = getKubeApiParams(await authSession(req.headers)); - const data = await deleteResource( - { - urlParams: { ...apiBaseParams, serverUrl, namespace }, - opts: requestOpts - }, - name - ); - - jsonRes(res, { - code: 200, - data - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/plugins/kubepanel/src/pages/api/kubernetes/create.ts b/frontend/plugins/kubepanel/src/pages/api/kubernetes/create.ts new file mode 100644 index 00000000000..5bba3eb114a --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/api/kubernetes/create.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; +import yaml from 'js-yaml'; +import { isString, merge } from 'lodash'; +import { getApiUrl } from '@/services/backend/api'; +import { mustGetTypedProperty } from '@/utils/api'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { authKubeConfig } from '@/services/backend/auth'; +import { handlerAxiosError, sendErrorResponse } from '@/services/backend/response'; +import { hasTypedProperty, isObject } from '@/k8slens/utilities'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { CreateQuery, CreateResponse } from '@/types/api/kubenertes'; + +function isCreateQuery(query: unknown): query is CreateQuery { + return isObject(query) && hasTypedProperty(query, 'kind', isString); +} + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + if (req.method !== 'POST') + throw buildErrno('Request Method is not allowed', ErrnoCode.UserMethodNotAllow); + + const { namespace, config } = authKubeConfig(req.headers); + if (!isCreateQuery(req.query)) + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + + const { kind } = req.query; + const url = getApiUrl(kind, namespace); + const createdData = yaml.load(mustGetTypedProperty(req.body, 'data', isString, 'string')); + + const data = merge(createdData, { + kind, + metadata: { + namespace + } + }); + + const res = await axios.post(url, data, config); + resp.status(res.status).json({ + code: res.status, + data: res.data + }); + } catch (err: any) { + sendErrorResponse( + resp, + handlerAxiosError(err, ErrnoCode.APICreateRequestError, ErrnoCode.APICreateResponseError) + ); + } +} diff --git a/frontend/plugins/kubepanel/src/pages/api/kubernetes/delete.ts b/frontend/plugins/kubepanel/src/pages/api/kubernetes/delete.ts new file mode 100644 index 00000000000..e5014444318 --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/api/kubernetes/delete.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { hasTypedProperty } from '@/k8slens/utilities'; +import { getApiUrl } from '@/services/backend/api'; +import { authKubeConfig } from '@/services/backend/auth'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { handlerAxiosError, sendErrorResponse } from '@/services/backend/response'; +import { DeleteQuery, DeleteResponse } from '@/types/api/kubenertes'; +import { isString, isObject } from 'lodash'; +import { NextApiRequest, NextApiResponse } from 'next'; + +function isDeleteQuery(query: unknown): query is DeleteQuery { + return ( + isObject(query) && + hasTypedProperty(query, 'kind', isString) && + hasTypedProperty(query, 'name', isString) + ); +} + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + if (req.method !== 'DELETE') + throw buildErrno('Request Method is not allowed', ErrnoCode.UserMethodNotAllow); + + const { namespace, config } = authKubeConfig(req.headers); + if (!isDeleteQuery(req.query)) + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + + const { kind, name } = req.query; + const url = getApiUrl(kind, namespace, name); + + const res = await axios.delete(url, config); + resp.status(res.status).json({ + code: res.status, + data: res.data + }); + } catch (err: any) { + sendErrorResponse( + resp, + handlerAxiosError(err, ErrnoCode.APIDeleteRequestError, ErrnoCode.APIDeleteResponseError) + ); + } +} diff --git a/frontend/plugins/kubepanel/src/pages/api/kubernetes/list.ts b/frontend/plugins/kubepanel/src/pages/api/kubernetes/list.ts new file mode 100644 index 00000000000..1b6476d623e --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/api/kubernetes/list.ts @@ -0,0 +1,45 @@ +import { hasOptionalTypedProperty, hasTypedProperty } from '@/k8slens/utilities'; +import { getApiUrl } from '@/services/backend/api'; +import { authKubeConfig } from '@/services/backend/auth'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { handlerAxiosError, sendErrorResponse } from '@/services/backend/response'; +import { ListQuery, ListResponse } from '@/types/api/kubenertes'; +import { KubeList } from '@/types/kube-resource'; +import axios from 'axios'; +import { isObject, isString } from 'lodash'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +function isListQuery(query: unknown): query is ListQuery { + return ( + isObject(query) && + hasTypedProperty(query, 'kind', isString) && + hasOptionalTypedProperty(query, 'limit', isString) + ); +} + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + if (req.method !== 'GET') + throw buildErrno('Request Method is not allowed', ErrnoCode.UserMethodNotAllow); + + const { namespace, config } = authKubeConfig(req.headers); + if (!isListQuery(req.query)) { + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + } + + // TODO: list limited items + const { kind, limit } = req.query; + const url = getApiUrl(kind, namespace); + + const res = await axios.get(url, config); + resp.status(res.status).json({ + code: res.status, + data: res.data as KubeList + }); + } catch (err: any) { + sendErrorResponse( + resp, + handlerAxiosError(err, ErrnoCode.APIListRequestError, ErrnoCode.APIListResponseError) + ); + } +} diff --git a/frontend/plugins/kubepanel/src/pages/api/kubernetes/template.ts b/frontend/plugins/kubepanel/src/pages/api/kubernetes/template.ts new file mode 100644 index 00000000000..b35e17b8c7b --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/api/kubernetes/template.ts @@ -0,0 +1,37 @@ +import fs from 'fs'; +import { sendErrorResponse } from '@/services/backend/response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { isObject, isString } from 'lodash'; +import { hasTypedProperty } from '@/k8slens/utilities'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { TemplateQuery, TemplateResponse } from '@/types/api/kubenertes'; +function isTemplateQuery(query: unknown): query is TemplateQuery { + return isObject(query) && hasTypedProperty(query, 'kind', isString); +} + +export default function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + if (req.method !== 'GET') + throw buildErrno('Request Method is not allowed', ErrnoCode.UserMethodNotAllow); + + if (!isTemplateQuery(req.query)) + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + + const { kind } = req.query; + const templatePath = `${process.cwd()}/public/create-resource-templates/${kind}.yaml`; + + if (!fs.existsSync(templatePath)) + throw buildErrno(`Template "${kind}" not found`, ErrnoCode.UserBadRequest); + + const template = fs.readFileSync(templatePath, 'utf8'); + if (template === '') + throw buildErrno(`Read template "${kind}" failed`, ErrnoCode.ServerInternalError); + + resp.status(200).json({ + code: 200, + data: template + }); + } catch (err: any) { + sendErrorResponse(resp, err); + } +} diff --git a/frontend/plugins/kubepanel/src/pages/api/kubernetes/update.ts b/frontend/plugins/kubepanel/src/pages/api/kubernetes/update.ts new file mode 100644 index 00000000000..be59538d60c --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/api/kubernetes/update.ts @@ -0,0 +1,52 @@ +import axios from 'axios'; +import yaml from 'js-yaml'; +import { authKubeConfig } from '@/services/backend/auth'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { mustGetTypedProperty } from '@/utils/api'; +import { isString, merge } from 'lodash'; +import { hasTypedProperty, isObject } from '@/k8slens/utilities'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { getApiUrl } from '@/services/backend/api'; +import { handlerAxiosError, sendErrorResponse } from '@/services/backend/response'; +import { UpdateQuery, UpdateResponse } from '@/types/api/kubenertes'; + +function isUpdateQuery(query: unknown): query is UpdateQuery { + return ( + isObject(query) && + hasTypedProperty(query, 'kind', isString) && + hasTypedProperty(query, 'name', isString) + ); +} + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + if (req.method !== 'PUT') + throw buildErrno('Request Method is not allowed', ErrnoCode.UserMethodNotAllow); + + const { namespace, config } = authKubeConfig(req.headers); + if (!isUpdateQuery(req.query)) + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + + const { kind, name } = req.query; + const url = getApiUrl(kind, namespace, name); + const updatedData = yaml.load(mustGetTypedProperty(req.body, 'data', isString, 'string')); + + const data = merge(updatedData, { + metadata: { + name, + namespace + } + }); + + const res = await axios.put(url, data, config); + resp.status(res.status).json({ + code: res.status, + data: res.data + }); + } catch (err: any) { + sendErrorResponse( + resp, + handlerAxiosError(err, ErrnoCode.APIUpdateRequestError, ErrnoCode.APIUpdateResponseError) + ); + } +} diff --git a/frontend/plugins/kubepanel/src/pages/api/kubernetes/watch.ts b/frontend/plugins/kubepanel/src/pages/api/kubernetes/watch.ts new file mode 100644 index 00000000000..7cefc5f607d --- /dev/null +++ b/frontend/plugins/kubepanel/src/pages/api/kubernetes/watch.ts @@ -0,0 +1,83 @@ +import axios from 'axios'; +import byline from 'byline'; +import { hasOptionalTypedProperty, hasTypedProperty } from '@/k8slens/utilities'; +import { getApiUrl } from '@/services/backend/api'; +import { authKubeConfig } from '@/services/backend/auth'; +import { ErrnoCode, buildErrno } from '@/services/backend/error'; +import { handlerAxiosError, sendErrorResponse } from '@/services/backend/response'; +import { WatchQuery, WatchResponse } from '@/types/api/kubenertes'; +import { isObject, isString } from 'lodash'; +import { NextApiRequest, NextApiResponse } from 'next'; + +function isWatchQuery(query: unknown): query is WatchQuery { + return ( + isObject(query) && + hasTypedProperty(query, 'kind', isString) && + hasOptionalTypedProperty(query, 'name', isString) + ); +} + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + if (req.method !== 'GET') + throw buildErrno('Request Method is not allowed', ErrnoCode.UserMethodNotAllow); + + const { namespace, config } = authKubeConfig(req.headers); + if (!isWatchQuery(req.query)) + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + + // TODO: watch specific item with its name + const { kind, name, resourceVersion } = req.query; + // if resourceVersion is empty string, it will list all items + if (resourceVersion === '') + throw buildErrno(`There has some invalid query in ${req.query}`, ErrnoCode.UserBadRequest); + + const url = getApiUrl(kind, namespace); + setSSEHeaders(resp); + const abortController = new AbortController(); + let eventId = 0; + + axios + .get(url, { + ...config, + params: { + resourceVersion, + watch: 1, + allowBookmarks: true + }, + signal: abortController.signal, + responseType: 'stream' + }) + .then((streamResp) => { + byline(streamResp.data).on('data', (line) => { + // check connection is open or not + if (!resp.writable) { + abortController.abort(); + return; + } + resp.write('event: watch\n'); + resp.write(`id: ${eventId++}\n`); + resp.write(`data: ${line}\n\n`); + }); + }) + .catch((err) => { + if (axios.isCancel(err)) return; + sendErrorResponse( + resp, + handlerAxiosError(err, ErrnoCode.APIWatchRequestError, ErrnoCode.APIWatchResponseError) + ); + abortController.abort(); + }); + } catch (err: any) { + sendErrorResponse(resp, err); + } +} + +function setSSEHeaders(resp: NextApiResponse) { + resp.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Content-Encoding': 'none', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive' + }); +} diff --git a/frontend/plugins/kubepanel/src/pages/api/list.ts b/frontend/plugins/kubepanel/src/pages/api/list.ts deleted file mode 100644 index 76642cfd850..00000000000 --- a/frontend/plugins/kubepanel/src/pages/api/list.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ApiBaseParamsMap } from '@/constants/kube-api'; -import { ResourceKey } from '@/constants/kube-object'; -import { listResource } from '@/services/backend/api'; -import { authSession } from '@/services/backend/auth'; -import { getKubeApiParams } from '@/services/backend/kubernetes'; -import { jsonRes } from '@/services/backend/response'; -import { ApiResp } from '@/services/kubernet'; -import { mustGetTypedProperty } from '@/utils/api'; -import { isError, isString } from 'lodash'; -import type { NextApiRequest, NextApiResponse } from 'next'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'GET') throw new Error(`Method not allowed: ${req.method}`); - - const resource = mustGetTypedProperty(req.query, 'resource', isString, 'string'); - - const { serverUrl, requestOpts, namespace } = getKubeApiParams(await authSession(req.headers)); - - const apiBaseParams = ApiBaseParamsMap[resource as ResourceKey]; - if (!apiBaseParams) { - throw new Error(`invalid resource ${resource}`); - } - - const { - code, - error = null, - data = null - } = await listResource({ - urlParams: { - ...apiBaseParams, - serverUrl, - namespace - }, - opts: requestOpts - }); - - if (isError(error)) throw error; - jsonRes(res, { - code, - data - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/plugins/kubepanel/src/pages/api/platform/init-data.ts b/frontend/plugins/kubepanel/src/pages/api/platform/init-data.ts index 3f39e0c09dc..390faa200e8 100644 --- a/frontend/plugins/kubepanel/src/pages/api/platform/init-data.ts +++ b/frontend/plugins/kubepanel/src/pages/api/platform/init-data.ts @@ -1,14 +1,15 @@ -import { jsonRes } from '@/services/backend/response'; import { NextApiRequest, NextApiResponse } from 'next'; export type Response = { SEALOS_DOMAIN: string; }; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - jsonRes(res, { - data: { - SEALOS_DOMAIN: process.env.SEALOS_DOMAIN || 'cloud.sealos.io' - } +export default async function handler( + req: NextApiRequest, + res: NextApiResponse> +) { + res.status(200).json({ + code: 200, + data: { SEALOS_DOMAIN: process.env.SEALOS_DOMAIN || 'cloud.sealos.io' } }); } diff --git a/frontend/plugins/kubepanel/src/pages/api/template.ts b/frontend/plugins/kubepanel/src/pages/api/template.ts deleted file mode 100644 index 379d5aec0f2..00000000000 --- a/frontend/plugins/kubepanel/src/pages/api/template.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { jsonRes } from '@/services/backend/response'; -import { ApiResp } from '@/services/kubernet'; -import { NextApiRequest, NextApiResponse } from 'next'; -import fs from 'fs'; -import { isString, startCase } from 'lodash'; -import { mustGetTypedProperty } from '@/utils/api'; - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const name = mustGetTypedProperty(req.query, 'name', isString, 'string'); - - const templatePath = `${process.cwd()}/public/create-resource-templates/${name}.yaml`; - - if (!fs.existsSync(templatePath)) throw new Error(`template "${startCase(name)}" not found`); - - const template = fs.readFileSync(templatePath, 'utf8'); - if (template === '') throw new Error(`read template "${startCase(name)}" failed`); - - jsonRes(res, { - code: 200, - data: template - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/plugins/kubepanel/src/pages/api/update.ts b/frontend/plugins/kubepanel/src/pages/api/update.ts deleted file mode 100644 index 5d26c55504f..00000000000 --- a/frontend/plugins/kubepanel/src/pages/api/update.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ApiBaseParamsMap } from '@/constants/kube-api'; -import { ResourceKey } from '@/constants/kube-object'; -import { updateResource } from '@/services/backend/api'; -import { authSession } from '@/services/backend/auth'; -import { getKubeApiParams } from '@/services/backend/kubernetes'; -import { jsonRes } from '@/services/backend/response'; -import { NextApiRequest, NextApiResponse } from 'next'; -import yaml from 'js-yaml'; -import { mustGetTypedProperty } from '@/utils/api'; -import { entries, isString } from 'lodash'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'PUT') throw new Error(`Method not allowed: ${req.method}`); - - const resource = mustGetTypedProperty(req.query, 'resource', isString, 'string'); - const name = mustGetTypedProperty(req.query, 'name', isString, 'string'); - - const apiBaseParams = ApiBaseParamsMap[resource as ResourceKey]; - if (!apiBaseParams) throw new Error(`invalid resource ${resource}`); - - const resourceData = yaml.load(mustGetTypedProperty(req.body, 'data', isString, 'string')); - - const { serverUrl, requestOpts, namespace } = getKubeApiParams(await authSession(req.headers)); - const data = await updateResource( - { - urlParams: { ...apiBaseParams, serverUrl, namespace }, - opts: requestOpts - }, - name, - resourceData - ); - console.log(data); - - jsonRes(res, { - code: 200, - data - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/action-button.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/action-button.tsx index 3d4a50712e6..a6c0bf2eb2f 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/action-button.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/action-button.tsx @@ -2,30 +2,21 @@ import { BarsOutlined, CloseOutlined, RetweetOutlined } from '@ant-design/icons' import { Button, type MenuProps, Dropdown } from 'antd'; import DeleteWarningModal from './delete-waring-modal'; import { Dispatch, SetStateAction, useCallback, useState } from 'react'; -import { ApiResp } from '@/services/kubernet'; import UpdateEditorModal from './update-editor-modal'; import { KubeObject } from '@/k8slens/kube-object'; interface Props { obj: K; - onDelete?: () => Promise; - onUpdate?: (data: string) => Promise; } -const ActionButton = ({ obj, onDelete, onUpdate }: Props) => { +const ActionButton = ({ obj }: Props) => { const [openDeleteModal, setOpenDeleteModal] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false); - const [childKey, setChildKey] = useState(0); - - const onClose = useCallback((setOpen: Dispatch>) => { - setOpen(false); - setChildKey(childKey + 1); - }, []); const items: MenuProps['items'] = [ { key: 'delete', - label: onDelete && ( + label: ( - {onDelete && ( - onClose(setOpenDeleteModal)} - onOk={() => onClose(setOpenDeleteModal)} - /> - )} - {onUpdate && ( - onClose(setOpenUpdateModal)} - onOk={() => onClose(setOpenUpdateModal)} - /> - )} + setOpenDeleteModal(false)} + onOk={() => setOpenDeleteModal(false)} + /> + setOpenUpdateModal(false)} + onOk={() => setOpenUpdateModal(false)} + /> ); }; diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/delete-waring-modal.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/delete-waring-modal.tsx index 763c9f99bde..460b66a0bb3 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/delete-waring-modal.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/delete-waring-modal.tsx @@ -1,56 +1,72 @@ -import { ApiResp } from '@/services/kubernet'; +import { deleteResource } from '@/api/kubernetes'; +import { KubeObject } from '@/k8slens/kube-object'; +import { buildErrorResponse } from '@/services/backend/response'; import { Button, Input, Modal, message } from 'antd'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; -interface Props { - targetName: string; +interface Props { + obj: K; open: boolean; - onDelete: () => Promise; onCancel: () => void; onOk: () => void; } -const DeleteWarningModal = ({ targetName, open, onDelete, onCancel, onOk }: Props) => { - const [isConfirmed, setIsConfirmed] = useState(false); - const [confirmed, setConfirmed] = useState(false); +const DeleteWarningModal = ({ + obj, + open, + onCancel, + onOk +}: Props) => { + if (!obj) return null; + const [isConfirmed, setIsConfirmed] = useState(false); const [msgApi, contextHolder] = message.useMessage(); - - useEffect(() => { - if (!confirmed) return; - const deleteRequest = async () => { - const resp = await onDelete(); - if (resp.code === 200) { - msgApi.success('Successfully deleted'); - } else { - msgApi.error(`Failed to delete: ${resp.data.message}`); - } - setConfirmed(false); - onOk(); - }; - - deleteRequest(); - }, [confirmed]); + const msgKey = 'deletedMsg'; return ( <> {contextHolder} Delete Warning} + open={open} + onCancel={onCancel} + destroyOnClose footer={[ , - ]} - open={open} - onCancel={onCancel} >

- Are you sure to delete {targetName}? + Are you sure to delete {obj.getName()}?

Please enter Confirm to confirm deletion: diff --git a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx index 9a66de24e25..a81c227eb3d 100644 --- a/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx +++ b/frontend/plugins/kubepanel/src/pages/kubepanel/components/action-button/update-editor-modal.tsx @@ -1,15 +1,15 @@ +import { updateResource } from '@/api/kubernetes'; import { KubeObject } from '@/k8slens/kube-object'; -import { ApiResp } from '@/services/kubernet'; +import { buildErrorResponse } from '@/services/backend/response'; import { dumpKubeObject } from '@/utils/yaml'; import { Editor } from '@monaco-editor/react'; import { Button, Modal, message } from 'antd'; import { editor } from 'monaco-editor'; -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; interface Props { obj?: K; open: boolean; - onUpdate: (data: string) => Promise; onCancel: () => void; onOk: () => void; } @@ -17,33 +17,15 @@ interface Props { const UpdateEditorModal = ({ obj, open, - onUpdate, onCancel, onOk }: Props) => { if (!obj) return null; - const [clickedUpdate, setClickedUpdate] = useState(false); const [msgApi, contextHolder] = message.useMessage(); + const msgKey = 'updatedMsg'; const editorRef = useRef(); - useEffect(() => { - if (!clickedUpdate || !editorRef.current) return; - const updateRequest = async () => { - const resp = await onUpdate(editorRef.current!.getValue()); - if (resp.code === 200) { - msgApi.success('Successfully updated'); - onOk(); - } else { - msgApi.error(`Failed to update: ${resp.data.message}`); - } - - setClickedUpdate(false); - }; - - updateRequest(); - }, [clickedUpdate]); - const editorValue = dumpKubeObject(obj); return ( @@ -53,8 +35,38 @@ const UpdateEditorModal = ({ title={

{`${obj.kind}: ${obj.getName()}`}
} open={open} onCancel={onCancel} + destroyOnClose footer={[ - ,