diff --git a/ui/apps/dashboard/locales/en-US.json b/ui/apps/dashboard/locales/en-US.json index 78f23151..535e043f 100644 --- a/ui/apps/dashboard/locales/en-US.json +++ b/ui/apps/dashboard/locales/en-US.json @@ -151,5 +151,7 @@ "f2224910b0d022374967254002eb756f": "Successfully edited scheduling strategy", "5863fd1d291adf46d804f5801a79d0e1": "Editing scheduling strategy failed", "79d5c80e3be24682145aa9246df18b40": "Is it a cluster?", - "85fe5099f6807dada65d274810933389": "Cluster" -} + "85fe5099f6807dada65d274810933389": "Cluster", + "c7961c290ec86485d8692f3c09b4075b": "New services added", + "8f3747c057d893862fbe4b7980e9b451": "Service Name" +} \ No newline at end of file diff --git a/ui/apps/dashboard/locales/zh-CN.json b/ui/apps/dashboard/locales/zh-CN.json index d61ebe82..14c9a3cc 100644 --- a/ui/apps/dashboard/locales/zh-CN.json +++ b/ui/apps/dashboard/locales/zh-CN.json @@ -150,5 +150,7 @@ "f2224910b0d022374967254002eb756f": "编辑调度策略成功", "5863fd1d291adf46d804f5801a79d0e1": "编辑调度策略失败", "79d5c80e3be24682145aa9246df18b40": "集群么?", - "85fe5099f6807dada65d274810933389": "集群" -} + "85fe5099f6807dada65d274810933389": "集群", + "c7961c290ec86485d8692f3c09b4075b": "新增服务", + "8f3747c057d893862fbe4b7980e9b451": "服务名称" +} \ No newline at end of file diff --git a/ui/apps/dashboard/src/layout/index.tsx b/ui/apps/dashboard/src/layout/index.tsx index e082e600..30c4d05d 100644 --- a/ui/apps/dashboard/src/layout/index.tsx +++ b/ui/apps/dashboard/src/layout/index.tsx @@ -5,7 +5,7 @@ import Header from './header'; import Sidebar from './sidebar'; import { cn } from '@/utils/cn.ts'; import { useAuth } from '@/components/auth'; -import { getSidebarWidth } from '@/utils/i18n.tsx'; +import { getSidebarWidth } from '@/utils/i18n'; import { useWindowSize } from "@uidotdev/usehooks"; const { Sider: AntdSider, Content: AntdContent } = AntdLayout; diff --git a/ui/apps/dashboard/src/layout/sidebar.tsx b/ui/apps/dashboard/src/layout/sidebar.tsx index b6870ee5..5c5fb170 100644 --- a/ui/apps/dashboard/src/layout/sidebar.tsx +++ b/ui/apps/dashboard/src/layout/sidebar.tsx @@ -9,7 +9,7 @@ import { import { useMatches, useNavigate } from 'react-router-dom'; import { FC, useMemo } from 'react'; import _ from 'lodash'; -import { getSidebarWidth } from '@/utils/i18n.tsx'; +import { getSidebarWidth } from '@/utils/i18n'; import { cn } from '@/utils/cn.ts'; import { useQuery } from '@tanstack/react-query'; import { GetDashboardConfig, menuConfig } from '@/services/dashboard-config.ts'; diff --git a/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/ingress-table.tsx b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/ingress-table.tsx new file mode 100644 index 00000000..e5ecb2d5 --- /dev/null +++ b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/ingress-table.tsx @@ -0,0 +1,146 @@ +import i18nInstance from '@/utils/i18n'; +import { Button, Popconfirm, Space, Table, TableColumnProps, Tag } from 'antd'; +import { + extractPropagationPolicy, + GetIngress, + Ingress, +} from '@/services/service.ts'; +import TagList from '@/components/tag-list'; +import { FC } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { GetResource } from '@/services/unstructured.ts'; +interface ServiceTableProps { + labelTagNum?: number; + selectedWorkSpace: string; + searchText: string; + onViewIngressContent: (r: any) => void; + onDeleteIngressContent: (r: Ingress) => void; +} +const IngressTable: FC = (props) => { + const { + labelTagNum, + selectedWorkSpace, + searchText, + onViewIngressContent, + onDeleteIngressContent, + } = props; + const columns: TableColumnProps[] = [ + { + title: i18nInstance.t('a4b28a416f0b6f3c215c51e79e517298'), + key: 'namespaceName', + width: 200, + render: (_, r) => { + return r.objectMeta.namespace; + }, + }, + { + title: i18nInstance.t('d7ec2d3fea4756bc1642e0f10c180cf5', '名称'), + key: 'ingressName', + width: 300, + render: (_, r) => { + return r.objectMeta.name; + }, + }, + { + title: i18nInstance.t('1f7be0a924280cd098db93c9d81ecccd'), + key: 'labelName', + align: 'left', + width: '30%', + render: (_, r) => { + if (!r?.objectMeta?.labels) { + return '-'; + } + const params = Object.keys(r.objectMeta.labels).map((key) => { + return { + key: `${r.objectMeta.name}-${key}`, + value: `${key}:${r.objectMeta.labels[key]}`, + }; + }); + return ; + }, + }, + { + title: i18nInstance.t('8a99082b2c32c843d2241e0ba60a3619'), + key: 'propagationPolicies', + render: (_, r) => { + const pp = extractPropagationPolicy(r); + return pp ? {pp} : '-'; + }, + }, + { + title: i18nInstance.t('eaf8a02d1b16fcf94302927094af921f'), + key: 'overridePolicies', + width: 150, + render: () => { + return '-'; + }, + }, + { + title: i18nInstance.t('2b6bc0f293f5ca01b006206c2535ccbc'), + key: 'op', + width: 200, + render: (_, r) => { + return ( + + + + + {}} + okText={i18nInstance.t('e83a256e4f5bb4ff8b3d804b5473217a')} + cancelText={i18nInstance.t('625fb26b4b3340f7872b411f401e754c')} + > + + + + ); + }, + }, + ]; + const { data, isLoading } = useQuery({ + queryKey: ['GetServices', selectedWorkSpace, searchText], + queryFn: async () => { + const services = await GetIngress({ + namespace: selectedWorkSpace, + keyword: searchText, + }); + return services.data || {}; + }, + }); + return ( + + `${r.objectMeta.namespace}-${r.objectMeta.name}` || '' + } + columns={columns} + loading={isLoading} + dataSource={data?.services || []} + /> + ); +}; +export default IngressTable; diff --git a/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/service-editor-modal.tsx b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/service-editor-modal.tsx new file mode 100644 index 00000000..ea4c0f86 --- /dev/null +++ b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/service-editor-modal.tsx @@ -0,0 +1,88 @@ +import i18nInstance from '@/utils/i18n'; +import { FC, useEffect, useState } from 'react'; +import { Modal } from 'antd'; +import Editor from '@monaco-editor/react'; +import { parse, stringify } from 'yaml'; +import _ from 'lodash'; +import { PutResource } from '@/services/unstructured'; +import { CreateDeployment } from '@/services/workload'; +import { IResponse } from '@/services/base.ts'; +export interface NewWorkloadEditorModalProps { + mode: 'create' | 'edit'; + open: boolean; + serviceContent?: string; + onOk: (ret: IResponse) => Promise | void; + onCancel: () => Promise | void; +} +const ServiceEditorModal: FC = (props) => { + const { mode, open, serviceContent = '', onOk, onCancel } = props; + const [content, setContent] = useState(serviceContent); + useEffect(() => { + console.log('workloadContent', serviceContent); + setContent(serviceContent); + }, [serviceContent]); + function handleEditorChange(value: string | undefined) { + setContent(value || ''); + } + return ( + { + // await onOk() + try { + const yamlObject = parse(content) as Record; + const kind = _.get(yamlObject, 'kind'); + const namespace = _.get(yamlObject, 'metadata.namespace'); + const name = _.get(yamlObject, 'metadata.name'); + if (mode === 'create') { + if (kind.toLowerCase() === 'deployment') { + const ret = await CreateDeployment({ + namespace, + name, + content: stringify(yamlObject), + }); + await onOk(ret); + setContent(''); + } + } else { + const ret = await PutResource({ + kind, + name, + namespace, + content: yamlObject, + }); + await onOk(ret); + setContent(''); + } + } catch (e) { + console.log('e', e); + } + }} + onCancel={async () => { + await onCancel(); + setContent(''); + }} + > + + + ); +}; +export default ServiceEditorModal; diff --git a/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/service-table.tsx b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/service-table.tsx new file mode 100644 index 00000000..a63a0ff0 --- /dev/null +++ b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/components/service-table.tsx @@ -0,0 +1,146 @@ +import i18nInstance from '@/utils/i18n'; +import { Button, Popconfirm, Space, Table, TableColumnProps, Tag } from 'antd'; +import { + extractPropagationPolicy, + GetServices, + Service, +} from '@/services/service.ts'; +import TagList from '@/components/tag-list'; +import { FC } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { GetResource } from '@/services/unstructured.ts'; +interface ServiceTableProps { + labelTagNum?: number; + selectedWorkSpace: string; + searchText: string; + onViewServiceContent: (r: any) => void; + onDeleteServiceContent: (r: Service) => void; +} +const ServiceTable: FC = (props) => { + const { + labelTagNum, + selectedWorkSpace, + searchText, + onViewServiceContent, + onDeleteServiceContent, + } = props; + const columns: TableColumnProps[] = [ + { + title: i18nInstance.t('a4b28a416f0b6f3c215c51e79e517298'), + key: 'namespaceName', + width: 200, + render: (_, r) => { + return r.objectMeta.namespace; + }, + }, + { + title: i18nInstance.t('8f3747c057d893862fbe4b7980e9b451', '服务名称'), + key: 'servicename', + width: 300, + render: (_, r) => { + return r.objectMeta.name; + }, + }, + { + title: i18nInstance.t('1f7be0a924280cd098db93c9d81ecccd'), + key: 'labelName', + align: 'left', + width: '30%', + render: (_, r) => { + if (!r?.objectMeta?.labels) { + return '-'; + } + const params = Object.keys(r.objectMeta.labels).map((key) => { + return { + key: `${r.objectMeta.name}-${key}`, + value: `${key}:${r.objectMeta.labels[key]}`, + }; + }); + return ; + }, + }, + { + title: i18nInstance.t('8a99082b2c32c843d2241e0ba60a3619'), + key: 'propagationPolicies', + render: (_, r) => { + const pp = extractPropagationPolicy(r); + return pp ? {pp} : '-'; + }, + }, + { + title: i18nInstance.t('eaf8a02d1b16fcf94302927094af921f'), + key: 'overridePolicies', + width: 150, + render: () => { + return '-'; + }, + }, + { + title: i18nInstance.t('2b6bc0f293f5ca01b006206c2535ccbc'), + key: 'op', + width: 200, + render: (_, r) => { + return ( + + + + + {}} + okText={i18nInstance.t('e83a256e4f5bb4ff8b3d804b5473217a')} + cancelText={i18nInstance.t('625fb26b4b3340f7872b411f401e754c')} + > + + + + ); + }, + }, + ]; + const { data, isLoading } = useQuery({ + queryKey: ['GetServices', selectedWorkSpace, searchText], + queryFn: async () => { + const services = await GetServices({ + namespace: selectedWorkSpace, + keyword: searchText, + }); + return services.data || {}; + }, + }); + return ( +
+ `${r.objectMeta.namespace}-${r.objectMeta.name}` || '' + } + columns={columns} + loading={isLoading} + dataSource={data?.services || []} + /> + ); +}; +export default ServiceTable; diff --git a/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/index.tsx b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/index.tsx index 27cb297f..b0921e4e 100644 --- a/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/index.tsx +++ b/ui/apps/dashboard/src/pages/multicloud-resource-manage/service/index.tsx @@ -1,10 +1,180 @@ +import i18nInstance from '@/utils/i18n'; import Panel from '@/components/panel'; +import { Button, Input, Segmented, Select } from 'antd'; +import { ServiceKind } from '@/services/base'; +import { Icons } from '@/components/icons'; +import { useQuery } from '@tanstack/react-query'; +import { GetNamespaces } from '@/services/namespace'; +import { useCallback, useMemo, useState } from 'react'; +import { useToggle, useWindowSize } from '@uidotdev/usehooks'; +import ServiceTable from './components/service-table'; +import ServiceEditorModal from './components/service-editor-modal'; +import { stringify } from 'yaml'; +import IngressTable from '@/pages/multicloud-resource-manage/service/components/ingress-table'; const ServicePage = () => { + const [filter, setFilter] = useState<{ + selectedWorkSpace: string; + searchText: string; + kind: ServiceKind; + }>({ + selectedWorkSpace: '', + searchText: '', + kind: ServiceKind.Service, + }); + const { data: nsData, isLoading: isNsDataLoading } = useQuery({ + queryKey: ['GetNamespaces'], + queryFn: async () => { + const clusters = await GetNamespaces({}); + return clusters.data || {}; + }, + }); + const nsOptions = useMemo(() => { + if (!nsData?.namespaces) return []; + return nsData.namespaces.map((item) => { + return { + title: item.objectMeta.name, + value: item.objectMeta.name, + }; + }); + }, [nsData]); + const size = useWindowSize(); + const labelTagNum = size && size.width! > 1800 ? undefined : 1; + const [editorState, setEditorState] = useState<{ + mode: 'create' | 'edit'; + content: string; + }>({ + mode: 'create', + content: '', + }); + const [showModal, toggleShowModal] = useToggle(false); + const resetEditorState = useCallback(() => { + setEditorState({ + mode: 'create', + content: '', + }); + }, []); return ( -

this is ServicePage

+
+
+ { + // reset filter when switch workload kind + if (value !== filter.kind) { + setFilter({ + ...filter, + kind: value, + selectedWorkSpace: '', + searchText: '', + }); + } else { + setFilter({ + ...filter, + kind: value, + }); + } + }} + /> +
+ +
+
+

+ {i18nInstance.t('280c56077360c204e536eb770495bc5f')} +

+