From ff26b697c71ee6c663d7597300e624a54e951682 Mon Sep 17 00:00:00 2001 From: zhujingyang <72259332+zjy365@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:52:53 +0800 Subject: [PATCH] feat:template support parsing option lists (#4533) * feat:template support parsing option lists Signed-off-by: jingyang <3161362058@qq.com> * fix --------- Signed-off-by: jingyang <3161362058@qq.com> --- frontend/desktop/deploy/manifests/rbac.yaml | 3 + frontend/desktop/src/api/platform.ts | 6 +- .../src/components/desktop_content/index.tsx | 20 +++ .../src/components/notification/index.tsx | 27 +--- .../src/pages/api/notification/global.ts | 41 ++++++ frontend/desktop/src/types/crd.ts | 26 ++++ .../template/src/components/Icon/index.tsx | 1 + .../template/src/components/Select/index.tsx | 134 +++++++++++++----- .../src/pages/deploy/components/Form.tsx | 42 +++++- .../src/pages/develop/components/Form.tsx | 42 +++++- .../template/src/pages/develop/index.tsx | 4 +- frontend/providers/template/src/types/app.ts | 3 +- .../providers/template/src/utils/json-yaml.ts | 2 + 13 files changed, 281 insertions(+), 70 deletions(-) create mode 100644 frontend/desktop/src/pages/api/notification/global.ts diff --git a/frontend/desktop/deploy/manifests/rbac.yaml b/frontend/desktop/deploy/manifests/rbac.yaml index 69d3d235983..e0a89f3d634 100644 --- a/frontend/desktop/deploy/manifests/rbac.yaml +++ b/frontend/desktop/deploy/manifests/rbac.yaml @@ -86,6 +86,9 @@ rules: - apiGroups: ['account.sealos.io'] resources: ['accounts'] verbs: ['list', 'get', 'create', 'update', 'patch', 'watch'] + - apiGroups: ['notification.sealos.io'] + resources: ['notifications'] + verbs: ['list', 'get', 'create', 'update', 'patch', 'watch'] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/frontend/desktop/src/api/platform.ts b/frontend/desktop/src/api/platform.ts index e2f845553e5..3f4c0abc9f7 100644 --- a/frontend/desktop/src/api/platform.ts +++ b/frontend/desktop/src/api/platform.ts @@ -1,5 +1,5 @@ import request from '@/services/request'; -import { ApiResp, Session, SystemConfigType, SystemEnv } from '@/types'; +import { ApiResp, NotificationItem, Session, SystemConfigType, SystemEnv } from '@/types'; import { AccountCRD } from '@/types/user'; // handle baidu @@ -53,3 +53,7 @@ export const getWechatResult = (payload: { code: string }) => request.get>('/api/auth/publicWechat/getWechatResult', { params: payload }); + +export const getGlobalNotification = () => { + return request.get>('/api/notification/global'); +}; diff --git a/frontend/desktop/src/components/desktop_content/index.tsx b/frontend/desktop/src/components/desktop_content/index.tsx index c9cde70e47c..56bcefcf4ca 100644 --- a/frontend/desktop/src/components/desktop_content/index.tsx +++ b/frontend/desktop/src/components/desktop_content/index.tsx @@ -12,6 +12,9 @@ import { MouseEvent, useCallback, useEffect, useState } from 'react'; import { createMasterAPP, masterApp } from 'sealos-desktop-sdk/master'; import IframeWindow from './iframe_window'; import styles from './index.module.scss'; +import { useQuery } from '@tanstack/react-query'; +import { getGlobalNotification } from '@/api/platform'; +import { useMessage } from '@sealos/ui'; const TimeComponent = dynamic(() => import('./time'), { ssr: false @@ -22,6 +25,7 @@ export default function DesktopContent(props: any) { const { installedApps: apps, runningInfo, openApp, setToHighestLayerById } = useAppStore(); const renderApps = apps.filter((item: TApp) => item?.displayType === 'normal'); const [maxItems, setMaxItems] = useState(10); + const { message } = useMessage(); const handleDoubleClick = (e: MouseEvent, item: TApp) => { e.preventDefault(); @@ -83,6 +87,22 @@ export default function DesktopContent(props: any) { const { UserGuide, showGuide } = useDriver({ openDesktopApp }); + useQuery(['getGlobalNotification'], getGlobalNotification, { + onSuccess(data) { + const newID = data.data?.metadata?.uid; + if (!newID || newID === localStorage.getItem('GlobalNotification')) return; + localStorage.setItem('GlobalNotification', newID); + const title = + i18n.language === 'zh' && data.data?.spec?.i18ns?.zh?.message + ? data.data?.spec?.i18ns?.zh?.message + : data.data?.spec?.message; + message({ + title: title, + status: 'info' + }); + } + }); + return ( + b?.spec?.timestamp - a?.spec?.timestamp; + + if (listCrd.body?.items) { + listCrd.body.items.sort(compareByTimestamp); + if (listCrd.body.items[0]) { + return jsonRes(res, { data: listCrd.body.items[0] }); + } + } + + jsonRes(res, { data: listCrd.body }); + } catch (err) { + jsonRes(res, { code: 500, data: err }); + } +} diff --git a/frontend/desktop/src/types/crd.ts b/frontend/desktop/src/types/crd.ts index 4e0b2d49663..74983f390ff 100644 --- a/frontend/desktop/src/types/crd.ts +++ b/frontend/desktop/src/types/crd.ts @@ -96,3 +96,29 @@ export type TAppCRList = { kind: 'AppList'; metadata: { continue: string; resourceVersion: string }; }; + +export type NotificationItem = { + metadata: { + creationTimestamp: string; + labels: { + isRead: string; + }; + name: string; + namespace: string; + uid: string; + }; + spec: { + from: string; + message: string; + timestamp: number; + title: string; + desktopPopup?: boolean; + i18ns?: { + zh?: { + from: string; + message: string; + title: string; + }; + }; + }; +}; diff --git a/frontend/providers/template/src/components/Icon/index.tsx b/frontend/providers/template/src/components/Icon/index.tsx index 3d3e8d2e716..61240861fad 100644 --- a/frontend/providers/template/src/components/Icon/index.tsx +++ b/frontend/providers/template/src/components/Icon/index.tsx @@ -50,5 +50,6 @@ const MyIcon = ({ ) : null; }; +export type IconType = keyof typeof map; export default MyIcon; diff --git a/frontend/providers/template/src/components/Select/index.tsx b/frontend/providers/template/src/components/Select/index.tsx index 68461b2ba7d..c3a7b4f5703 100644 --- a/frontend/providers/template/src/components/Select/index.tsx +++ b/frontend/providers/template/src/components/Select/index.tsx @@ -1,21 +1,34 @@ -import React from 'react'; -import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure } from '@chakra-ui/react'; +import React, { useRef, forwardRef, useMemo } from 'react'; +import { + Menu, + Box, + MenuList, + MenuItem, + Button, + useDisclosure, + useOutsideClick +} from '@chakra-ui/react'; import type { ButtonProps } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; -import { useTranslation } from 'next-i18next'; +import MyIcon, { type IconType } from '../Icon'; + interface Props extends ButtonProps { value?: string; placeholder?: string; list: { - label: string; - id: string; + icon?: string; + label: string | React.ReactNode; + value: string; }[]; - width?: number | string; onchange?: (val: string) => void; } -const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props }: Props) => { - const { t } = useTranslation(); +const MySelect = ( + { placeholder, value, width = 'auto', list, onchange, ...props }: Props, + selectRef: any +) => { + const ref = useRef(null); + const SelectRef = useRef(null); const menuItemStyles = { borderRadius: 'sm', py: 2, @@ -27,56 +40,99 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props }; const { isOpen, onOpen, onClose } = useDisclosure(); + useOutsideClick({ + ref: SelectRef, + handler: () => { + onClose(); + } + }); + + const activeMenu = useMemo(() => list.find((item) => item.value === value), [list, value]); + return ( - - + + { + isOpen ? onClose() : onOpen(); + }} + > - - - {list.map((item) => ( - { + const w = ref.current?.clientWidth; + if (w) { + return `${w}px !important`; + } + return Array.isArray(width) + ? width.map((item) => `${item} !important`) + : `${width} !important`; + })()} + p={'6px'} + border={'1px solid #fff'} + boxShadow={ + '0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);' + } + zIndex={99} + transform={'translateY(35px) !important'} + > + {list.map((item) => ( + { + if (onchange && value !== item.value) { + onchange(item.value); } - : {})} - onClick={() => { - if (onchange && value !== item.id) { - onchange(item.id); - } - }} - > - {t(item.label)} - - ))} - + }} + > + {!!item.icon && } + {item.label} + + ))} + + ); }; -export default MySelect; +export default React.memo(forwardRef(MySelect)); diff --git a/frontend/providers/template/src/pages/deploy/components/Form.tsx b/frontend/providers/template/src/pages/deploy/components/Form.tsx index f7d61f6ad1f..70b2368977a 100644 --- a/frontend/providers/template/src/pages/deploy/components/Form.tsx +++ b/frontend/providers/template/src/pages/deploy/components/Form.tsx @@ -1,10 +1,11 @@ import MyIcon from '@/components/Icon'; +import MySelect from '@/components/Select'; import type { QueryType } from '@/types'; import { FormSourceInput } from '@/types/app'; import { Box, Flex, FormControl, Input, Text, useTheme } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo } from 'react'; import { UseFormReturn } from 'react-hook-form'; const Form = ({ @@ -54,6 +55,45 @@ const Form = ({ {isShowContent ? ( {formSource?.inputs?.map((item: FormSourceInput, index: number) => { + if (item.type === 'choice' && item.options) { + return ( + + + + {item?.label} + {item?.required && ( + + * + + )} + + + { + return { + value: option, + label: option + }; + })} + onchange={(val: any) => { + formHook.setValue(item.key, val); + }} + /> + + + + ); + } return ( diff --git a/frontend/providers/template/src/pages/develop/components/Form.tsx b/frontend/providers/template/src/pages/develop/components/Form.tsx index 7dfa73ebb06..b46ede4d2cd 100644 --- a/frontend/providers/template/src/pages/develop/components/Form.tsx +++ b/frontend/providers/template/src/pages/develop/components/Form.tsx @@ -1,9 +1,10 @@ import MyIcon from '@/components/Icon'; +import MySelect from '@/components/Select'; import { FormSourceInput, TemplateSourceType } from '@/types/app'; import { Box, Flex, FormControl, Input, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useMemo } from 'react'; -import { UseFormReturn, useForm } from 'react-hook-form'; +import { UseFormReturn } from 'react-hook-form'; const Form = ({ formSource, @@ -29,6 +30,45 @@ const Form = ({ {isShowContent ? ( {formSource?.source?.inputs?.map((item: FormSourceInput, index: number) => { + if (item.type === 'choice' && item.options) { + return ( + + + + {item?.label} + {item?.required && ( + + * + + )} + + + { + return { + value: option, + label: option + }; + })} + onchange={(val: any) => { + formHook.setValue(item.key, val); + }} + /> + + + + ); + } return ( diff --git a/frontend/providers/template/src/pages/develop/index.tsx b/frontend/providers/template/src/pages/develop/index.tsx index bfd902e5a04..664771ca97e 100644 --- a/frontend/providers/template/src/pages/develop/index.tsx +++ b/frontend/providers/template/src/pages/develop/index.tsx @@ -34,6 +34,7 @@ import Editor from './components/Editor'; export default function Develop() { const { t } = useTranslation(); const { toast } = useToast(); + const [forceUpdate, setForceUpdate] = useState(false); const [yamlSource, setYamlSource] = useState(); const [yamlList, setYamlList] = useState([]); const { Loading, setIsLoading } = useLoading(); @@ -97,7 +98,7 @@ export default function Develop() { const formInputs = formHook.getValues(); setYamlSource(result); - const correctYamlList = generateCorrectYamlList(result, formInputs); + const correctYamlList = generateCorrectYamlList(result, { ...defaultInputes, ...formInputs }); setYamlList(correctYamlList); } catch (error: any) { toast({ @@ -118,6 +119,7 @@ export default function Develop() { // watch form change, compute new yaml formHook.watch((data: any) => { data && formOnchangeDebounce(data); + setForceUpdate(!forceUpdate); }); const formOnchangeDebounce = debounce((data: any) => { diff --git a/frontend/providers/template/src/types/app.ts b/frontend/providers/template/src/types/app.ts index c2bca450c74..9d64d63aa18 100644 --- a/frontend/providers/template/src/types/app.ts +++ b/frontend/providers/template/src/types/app.ts @@ -82,7 +82,8 @@ export type FormSourceInput = { key: string; label: string; required: boolean; - type: string; + type: string; // string | number | 'choice' | boolean; + options?: string[]; }; export type TemplateInstanceType = { diff --git a/frontend/providers/template/src/utils/json-yaml.ts b/frontend/providers/template/src/utils/json-yaml.ts index 2fe33446089..445a9c9cb56 100644 --- a/frontend/providers/template/src/utils/json-yaml.ts +++ b/frontend/providers/template/src/utils/json-yaml.ts @@ -95,6 +95,7 @@ export const getTemplateDataSource = ( type: string; default: string; required: boolean; + options?: string[]; } >, cloneDefauls: Record< @@ -118,6 +119,7 @@ export const getTemplateDataSource = ( } const output = mapValues(cloneDefauls, (value) => value.value); return { + ...item, description: parseTemplateString(item.description, /\$\{\{\s*(.*?)\s*\}\}/g, { ...platformEnvs, defaults: output,