From bf5379f6d98047eb7b915a6c56c0acc33eb0f3c5 Mon Sep 17 00:00:00 2001 From: zhujingyang <72259332+zjy365@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:34:09 +0800 Subject: [PATCH] feat:desktop notification (#4371) * feat:desktop notification Signed-off-by: jingyang <3161362058@qq.com> * fix Signed-off-by: jingyang <3161362058@qq.com> --------- Signed-off-by: jingyang <3161362058@qq.com> --- .../desktop/public/locales/en/common.json | 5 +- .../desktop/public/locales/zh/common.json | 7 +- .../src/components/notification/index.tsx | 325 +++++++++++++++--- .../ui/src/components/icons/CloseIcon.tsx | 20 ++ .../ui/src/components/icons/WarnIcon.tsx | 16 + frontend/packages/ui/src/components/index.ts | 7 +- 6 files changed, 325 insertions(+), 55 deletions(-) create mode 100644 frontend/packages/ui/src/components/icons/CloseIcon.tsx create mode 100644 frontend/packages/ui/src/components/icons/WarnIcon.tsx diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 8043282080c..821f7ec6a34 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -99,5 +99,8 @@ "Please read and agree to the agreement": "Please read and agree to the agreement", "Purchase Link Error": "Purchase Link Error", "You have not purchased the License": "You have not purchased the License", - "App Info": "App Info" + "App Info": "App Info", + "Click anywhere to continue": "Click on any blank space to continue", + "Jump Over": "Jump Over", + "Detail": "Detail" } \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index 41a1a78fe5a..3437909129c 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -95,5 +95,8 @@ "Please read and agree to the agreement": "请阅读并同意协议", "Purchase Link Error": "购买链接错误", "You have not purchased the License": "您还没有购买 License", - "App Info": "应用信息" -} + "App Info": "应用信息", + "Click anywhere to continue": "点击任意空白继续", + "Jump Over": "跳过", + "Detail": "详情" +} \ No newline at end of file diff --git a/frontend/desktop/src/components/notification/index.tsx b/frontend/desktop/src/components/notification/index.tsx index 3753960bc83..23ad162f8e5 100644 --- a/frontend/desktop/src/components/notification/index.tsx +++ b/frontend/desktop/src/components/notification/index.tsx @@ -1,14 +1,16 @@ -import { Box, Flex, Text, Img, UseDisclosureReturn, Button } from '@chakra-ui/react'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import clsx from 'clsx'; import Iconfont from '@/components/iconfont'; -import { useMemo, useState } from 'react'; import request from '@/services/request'; +import useAppStore from '@/stores/app'; import { formatTime } from '@/utils/tools'; -import styles from './index.module.scss'; +import { Box, Button, Flex, Text, UseDisclosureReturn } from '@chakra-ui/react'; +import { ClearOutlineIcon, CloseIcon, WarnIcon } from '@sealos/ui'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import clsx from 'clsx'; +import { produce } from 'immer'; import { useTranslation } from 'next-i18next'; -// import warnIcon from 'public/icons/clear-outlined.svg'; -import { ClearOutlineIcon } from '@sealos/ui'; +import { useEffect, useState } from 'react'; +import styles from './index.module.scss'; + type NotificationItem = { metadata: { creationTimestamp: string; @@ -24,6 +26,14 @@ type NotificationItem = { message: string; timestamp: number; title: string; + desktopPopup?: boolean; + i18ns?: { + zh?: { + from: string; + message: string; + title: string; + }; + }; }; }; @@ -33,40 +43,61 @@ type TNotification = { }; export default function Notification(props: TNotification) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { disclosure, onAmount } = props; - const [activeTab, setActiveTab] = useState<'read' | 'unread'>('unread'); - const [activePage, setActivePage] = useState<'index' | 'detail'>('index'); - const [msgDetail, setMsgDetail] = useState(); - const [notification, setNotification] = useState([]); + const { installedApps, openApp } = useAppStore(); + const [readNotes, setReadNotes] = useState([]); + const [unReadNotes, setUnReadNotes] = useState([]); + + const [MessageConfig, setMessageConfig] = useState<{ + activeTab: 'read' | 'unread'; + activePage: 'index' | 'detail'; + msgDetail?: NotificationItem; + popupMessage?: NotificationItem; + }>({ + activeTab: 'unread', + activePage: 'index', + msgDetail: undefined, + popupMessage: undefined + }); - const { refetch } = useQuery(['getAwsAll'], () => request('/api/notification/list'), { + const { refetch } = useQuery(['getNotifications'], () => request('/api/notification/list'), { onSuccess: (data) => { - onAmount( - data?.data?.items?.filter( - (item: NotificationItem) => !JSON.parse(item?.metadata?.labels?.isRead || 'false') - )?.length || 0 - ); - setNotification(data?.data?.items); + const messages = data?.data?.items as NotificationItem[]; + if (messages) { + handleNotificationData(messages); + } }, - refetchInterval: 1 * 60 * 1000 + refetchInterval: 5 * 60 * 1000 }); - const [unread_notes, read_notes] = useMemo(() => { - const unread: NotificationItem[] = []; - const read: NotificationItem[] = []; + const handleNotificationData = (data: NotificationItem[]) => { + const parseIsRead = (item: NotificationItem) => + JSON.parse(item?.metadata?.labels?.isRead || 'false'); - notification?.forEach((item: NotificationItem) => { - JSON.parse(item?.metadata?.labels?.isRead || 'false') ? read.push(item) : unread.push(item); - }); + const unReadMessage = data.filter((item) => !parseIsRead(item)); + const readMessage = data.filter(parseIsRead); const compareByTimestamp = (a: NotificationItem, b: NotificationItem) => b?.spec?.timestamp - a?.spec?.timestamp; - return [unread.sort(compareByTimestamp), read.sort(compareByTimestamp)]; - }, [notification]); + unReadMessage.sort(compareByTimestamp); + readMessage.sort(compareByTimestamp); - const notifications = activeTab === 'unread' ? unread_notes : read_notes; + if (unReadMessage?.[0]?.spec?.desktopPopup) { + setMessageConfig( + produce((draft) => { + draft.popupMessage = unReadMessage[0]; + }) + ); + } + + onAmount(unReadMessage?.length || 0); + setReadNotes(readMessage); + setUnReadNotes(unReadMessage); + }; + + const notifications = MessageConfig.activeTab === 'unread' ? unReadNotes : readNotes; const readMsgMutation = useMutation({ mutationFn: (name: string[]) => request.post('/api/notification/read', { name }), @@ -74,21 +105,58 @@ export default function Notification(props: TNotification) { }); const goMsgDetail = (item: NotificationItem) => { - if (activeTab === 'unread') { + if (MessageConfig.activeTab === 'unread') { readMsgMutation.mutate([item?.metadata?.name]); } - setActivePage('detail'); - setMsgDetail(item); + setMessageConfig( + produce((draft) => { + draft.activePage = 'detail'; + draft.msgDetail = item; + draft.popupMessage = undefined; + }) + ); }; const markAllAsRead = () => { - const names = unread_notes?.map((item: NotificationItem) => item?.metadata?.name); + const names = unReadNotes?.map((item: NotificationItem) => item?.metadata?.name); readMsgMutation.mutate(names); + setMessageConfig( + produce((draft) => { + draft.popupMessage = undefined; + }) + ); + }; + + const handleCharge = () => { + const costCenter = installedApps.find((i) => i.key === 'system-costcenter'); + if (!costCenter) return; + openApp(costCenter, { + query: { + openRecharge: 'true' + } + }); }; + const resetMessageState = () => { + setMessageConfig( + produce((draft) => { + draft.activeTab = 'unread'; + draft.activePage = 'index'; + draft.msgDetail = undefined; + }) + ); + disclosure.onClose(); + }; + + useEffect(() => { + if (i18n.language) { + refetch(); + } + }, [i18n.language, refetch]); + return disclosure.isOpen ? ( <> - + setActivePage('index')} - data-active={activePage} + onClick={() => + setMessageConfig( + produce((draft) => { + draft.activePage = 'index'; + }) + ) + } + data-active={MessageConfig.activePage} > - {activePage === 'index' ? t('Message Center') : msgDetail?.spec?.title} + + {MessageConfig.activePage === 'index' + ? t('Message Center') + : i18n.language === 'zh' && MessageConfig.msgDetail?.spec?.i18ns?.zh?.title + ? MessageConfig.msgDetail?.spec?.i18ns?.zh?.title + : MessageConfig.msgDetail?.spec?.title} + - {activePage === 'index' ? ( + {MessageConfig.activePage === 'index' ? ( <> setActiveTab('unread')} + className={clsx(MessageConfig.activeTab === 'unread' && styles.active, styles.tab)} + onClick={() => + setMessageConfig( + produce((draft) => { + draft.activeTab = 'unread'; + }) + ) + } > - {t('Unread')} ({unread_notes?.length || 0}) + {t('Unread')} ({unReadNotes?.length || 0}) setActiveTab('read')} + className={clsx(MessageConfig.activeTab === 'read' && styles.active, styles.tab)} + onClick={() => + setMessageConfig( + produce((draft) => { + draft.activeTab = 'read'; + }) + ) + } > {t('Have Read')} @@ -144,9 +236,15 @@ export default function Notification(props: TNotification) { key={item?.metadata?.uid} onClick={() => goMsgDetail(item)} > - {item?.spec?.title} + + {i18n.language === 'zh' && item.spec?.i18ns?.zh?.title + ? item.spec?.i18ns?.zh?.title + : item?.spec?.title} + - {item?.spec?.message} + {i18n.language === 'zh' && item.spec?.i18ns?.zh?.message + ? item.spec?.i18ns?.zh?.message + : item?.spec?.message} - {t('From')}「{item?.spec?.from}」 + {t('From')}「 + {i18n.language === 'zh' && item.spec?.i18ns?.zh?.from + ? item.spec?.i18ns?.zh?.from + : item?.spec?.from} + 」 {formatTime((item?.spec?.timestamp || 0) * 1000, 'YYYY-MM-DD HH:mm')} @@ -181,10 +283,17 @@ export default function Notification(props: TNotification) { fontWeight="400" > - {t('From')}「{msgDetail?.spec?.from}」 + {t('From')}「 + {i18n.language === 'zh' && MessageConfig.msgDetail?.spec?.i18ns?.zh?.from + ? MessageConfig.msgDetail?.spec?.i18ns?.zh?.from + : MessageConfig.msgDetail?.spec?.from} + 」 - {formatTime((msgDetail?.spec?.timestamp || 0) * 1000, 'YYYY-MM-DD HH:mm')} + {formatTime( + (MessageConfig.msgDetail?.spec?.timestamp || 0) * 1000, + 'YYYY-MM-DD HH:mm' + )} - {msgDetail?.spec?.message} + {i18n.language === 'zh' && MessageConfig.msgDetail?.spec?.i18ns?.zh?.message + ? MessageConfig.msgDetail?.spec?.i18ns?.zh?.message + : MessageConfig.msgDetail?.spec?.message} + {MessageConfig.msgDetail?.spec?.from === 'Debt-System' && ( + + + + )} )} ) : ( - <> + <> + {MessageConfig?.popupMessage && ( + + + + + {i18n.language === 'zh' && MessageConfig.popupMessage?.spec?.i18ns?.zh?.title + ? MessageConfig.popupMessage?.spec?.i18ns?.zh?.title + : MessageConfig.popupMessage?.spec?.title} + + { + setMessageConfig( + produce((draft) => { + draft.popupMessage = undefined; + }) + ); + }} + /> + + + {i18n.language === 'zh' && MessageConfig.popupMessage?.spec?.i18ns?.zh?.message + ? MessageConfig.popupMessage?.spec?.i18ns?.zh?.message + : MessageConfig.popupMessage?.spec?.message} + + + + + + + + )} + ); } diff --git a/frontend/packages/ui/src/components/icons/CloseIcon.tsx b/frontend/packages/ui/src/components/icons/CloseIcon.tsx new file mode 100644 index 00000000000..4e5e5854ff5 --- /dev/null +++ b/frontend/packages/ui/src/components/icons/CloseIcon.tsx @@ -0,0 +1,20 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export default function CloseIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/frontend/packages/ui/src/components/icons/WarnIcon.tsx b/frontend/packages/ui/src/components/icons/WarnIcon.tsx new file mode 100644 index 00000000000..22bbe6446d5 --- /dev/null +++ b/frontend/packages/ui/src/components/icons/WarnIcon.tsx @@ -0,0 +1,16 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export default function WarnIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/frontend/packages/ui/src/components/index.ts b/frontend/packages/ui/src/components/index.ts index b3bae79a837..8655d9100e1 100644 --- a/frontend/packages/ui/src/components/index.ts +++ b/frontend/packages/ui/src/components/index.ts @@ -38,6 +38,9 @@ import GoogleIcon from './icons/GoogleIcon'; import WechatIcon from './icons/WechatIcon'; import ListIcon from './icons/ListIcon'; import PortIcon from './icons/PortIcon'; +import WarnIcon from './icons/WarnIcon'; +import CloseIcon from './icons/CloseIcon'; + export { YamlCode, EditTabs, @@ -78,5 +81,7 @@ export { UploadIcon, VisibityIcon, WechatIcon, - PortIcon + PortIcon, + WarnIcon, + CloseIcon };