diff --git a/Dockerfile b/Dockerfile index d6ef092..df713d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN npm ci COPY . . RUN VITE_API_BASE_URL=/v1/ npm run build -FROM nginx:1.30.0-alpine +FROM nginx:1.31.0-alpine RUN apk upgrade --no-cache diff --git a/package-lock.json b/package-lock.json index c307454..0eff1d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@tailwindcss/vite": "^4.1.17", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.24", "ace-builds": "^1.43.6", "ansi_up": "^6.0.6", "axios": "^1.15.0", @@ -56,7 +57,7 @@ "i18next": "^26.0.4", "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.563.0", + "lucide-react": "^1.14.0", "motion": "^12.34.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -67,7 +68,7 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.72.1", "react-i18next": "^17.0.2", - "react-resizable-panels": "^4.8.0", + "react-resizable-panels": "^4.11.0", "react-router-dom": "^7.13.0", "react-scan": "^0.5.3", "react-spring": "^10.0.3", @@ -82,7 +83,7 @@ "zustand": "^5.0.12" }, "devDependencies": { - "@eslint/js": "^9.39.1", + "@eslint/js": "^10.0.1", "@types/node": "^25.0.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.2", @@ -452,16 +453,24 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { @@ -3635,6 +3644,23 @@ "react-dom": ">=16.8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz", + "integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.14.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@tanstack/table-core": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", @@ -3648,6 +3674,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", + "integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -6765,9 +6801,9 @@ } }, "node_modules/lucide-react": { - "version": "0.563.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", - "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8278,9 +8314,9 @@ } }, "node_modules/react-resizable-panels": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.10.0.tgz", - "integrity": "sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.11.0.tgz", + "integrity": "sha512-LPk/AkFDGkg7SsbOyL93ojrE6E7lhrxxDwnYNjfmnSeI6BE7Sje6dB24PXgZk8DeugdeXNk1LO+ohRqIjhxiLw==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0", diff --git a/package.json b/package.json index 9628b37..cced0fa 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@tailwindcss/vite": "^4.1.17", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.24", "ace-builds": "^1.43.6", "ansi_up": "^6.0.6", "axios": "^1.15.0", @@ -58,7 +59,7 @@ "i18next": "^26.0.4", "i18next-browser-languagedetector": "^8.2.1", "input-otp": "^1.4.2", - "lucide-react": "^0.563.0", + "lucide-react": "^1.14.0", "motion": "^12.34.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -69,7 +70,7 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.72.1", "react-i18next": "^17.0.2", - "react-resizable-panels": "^4.8.0", + "react-resizable-panels": "^4.11.0", "react-router-dom": "^7.13.0", "react-scan": "^0.5.3", "react-spring": "^10.0.3", @@ -84,7 +85,7 @@ "zustand": "^5.0.12" }, "devDependencies": { - "@eslint/js": "^9.39.1", + "@eslint/js": "^10.0.1", "@types/node": "^25.0.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.2", diff --git a/src/api/client.ts b/src/api/client.ts index 4a8b79e..b1f5724 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -57,12 +57,8 @@ axiosInstance.interceptors.response.use( }, async (error: AxiosError) => { const originalRequest = error.config as AuthenticationRequestConfig; - const {response} = error || {}; - if (response?.status === 401) { - userStore.getState().actions.clearUserToken(); - return Promise.reject(error); - } - if (response?.status === 417 && !originalRequest._retry) { + const status = error?.response?.status; + if ((status === 401 || status === 417) && !originalRequest._retry) { return refresh(originalRequest, error); } return Promise.reject(error); diff --git a/src/global.css b/src/global.css index d92551b..fd64dce 100644 --- a/src/global.css +++ b/src/global.css @@ -215,6 +215,10 @@ @apply border-border outline-ring/50; } + html { + font-size: 85%; + } + body { @apply bg-background text-foreground; } diff --git a/src/hooks/use-cron-job.ts b/src/hooks/use-cron-job.ts index 1c37d0c..99ea219 100644 --- a/src/hooks/use-cron-job.ts +++ b/src/hooks/use-cron-job.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import cronJobService, {type CronJob} from "@/api/services/cron-job-service.ts"; @@ -26,7 +27,7 @@ export default function useCronJob() { } loadCronJob(); - const interval = window.setInterval(loadCronJob, 3000); + const interval = window.setInterval(loadCronJob, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/hooks/use-daemon-set.ts b/src/hooks/use-daemon-set.ts index 329968c..7d30fee 100644 --- a/src/hooks/use-daemon-set.ts +++ b/src/hooks/use-daemon-set.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import daemonSetService, {type DaemonSet} from "@/api/services/daemon-set-service.ts"; @@ -26,7 +27,7 @@ export default function useDaemonSet() { } loadDaemonSet(); - const interval = window.setInterval(loadDaemonSet, 3000); + const interval = window.setInterval(loadDaemonSet, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/hooks/use-deployment.ts b/src/hooks/use-deployment.ts index ccf5813..3feaaac 100644 --- a/src/hooks/use-deployment.ts +++ b/src/hooks/use-deployment.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import deploymentService, {type Deployment} from "@/api/services/deployment-service.ts"; @@ -26,7 +27,7 @@ export default function useDeployment() { } loadDeployment(); - const interval = window.setInterval(loadDeployment, 3000); + const interval = window.setInterval(loadDeployment, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/hooks/use-node.ts b/src/hooks/use-node.ts index dc13521..acd27bd 100644 --- a/src/hooks/use-node.ts +++ b/src/hooks/use-node.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import nodeService, {type Node} from "@/api/services/node-service.ts"; @@ -25,7 +26,7 @@ export default function useNode() { } loadNode(); - const interval = window.setInterval(loadNode, 3000); + const interval = window.setInterval(loadNode, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/hooks/use-pod.ts b/src/hooks/use-pod.ts index b68518a..164599c 100644 --- a/src/hooks/use-pod.ts +++ b/src/hooks/use-pod.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import podService, {type Pod} from "@/api/services/pod-service.ts"; @@ -26,7 +27,7 @@ export default function usePod() { } loadPod(); - const interval = window.setInterval(loadPod, 3000); + const interval = window.setInterval(loadPod, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/hooks/use-replica-set.ts b/src/hooks/use-replica-set.ts index 9e51cf8..d2c4022 100644 --- a/src/hooks/use-replica-set.ts +++ b/src/hooks/use-replica-set.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import replicaSetService, {type ReplicaSet} from "@/api/services/replica-set-service.ts"; @@ -26,7 +27,7 @@ export default function useReplicaSet() { } loadReplicaSet(); - const interval = window.setInterval(loadReplicaSet, 3000); + const interval = window.setInterval(loadReplicaSet, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/hooks/use-stateful-set.ts b/src/hooks/use-stateful-set.ts index 98cdf54..85c7f65 100644 --- a/src/hooks/use-stateful-set.ts +++ b/src/hooks/use-stateful-set.ts @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useSearchParams} from "react-router-dom"; import {useEffect, useState} from "react"; import statefulSetService, {type StatefulSet} from "@/api/services/stateful-set-service.ts"; @@ -26,7 +27,7 @@ export default function useStatefulSet() { } loadStatefulSet(); - const interval = window.setInterval(loadStatefulSet, 3000); + const interval = window.setInterval(loadStatefulSet, POLL_INTERVAL_MS); return () => { isMounted = false; clearInterval(interval); }; }, [searchParams]); diff --git a/src/layouts/panel/header/notification/sheet.tsx b/src/layouts/panel/header/notification/sheet.tsx index 711b3cf..25af910 100644 --- a/src/layouts/panel/header/notification/sheet.tsx +++ b/src/layouts/panel/header/notification/sheet.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import { Sheet, SheetContent, @@ -10,6 +11,7 @@ import {AvatarFallback} from "@radix-ui/react-avatar"; import {Bell} from "lucide-react"; import {Badge} from "@/components/ui/badge.tsx"; import * as React from "react"; +import {useVirtualizer} from "@tanstack/react-virtual"; import NotificationService, { type Notification } from "@/api/services/notification-service.ts"; @@ -19,6 +21,7 @@ import NotificationAlert from "@/layouts/panel/header/notification/alert.tsx"; export default function NotificationSheet({cluster}: {cluster: boolean}) { const {t} = useTranslation(); const [notifications, setNotifications] = React.useState([]); + const scrollRef = React.useRef(null); React.useEffect(() => { const fetchNotifications = () => { @@ -27,15 +30,30 @@ export default function NotificationSheet({cluster}: {cluster: boolean}) { : NotificationService.listAll(); result.then(({ notifications }) => { - setNotifications(notifications); + setNotifications(notifications ?? []); }); }; fetchNotifications(); - const interval = setInterval(fetchNotifications, 3000); + const interval = setInterval(fetchNotifications, POLL_INTERVAL_MS); return () => clearInterval(interval); }, [cluster]); + const virtualizer = useVirtualizer({ + count: notifications.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 120, + overscan: 5, + }); + + const handleSeen = React.useCallback((id: string) => { + setNotifications(prev => + prev.map(n => + n.id === id ? { ...n, state: "SEEN" } : n + ) + ); + }, []); + const unseenCount = notifications.filter(n => n.state !== "SEEN").length; return ( @@ -62,29 +80,36 @@ export default function NotificationSheet({cluster}: {cluster: boolean}) { {t("panel.header.notifications.title")} -
-
    - {notifications.length === 0 && ( -

    - {t("panel.header.notifications.empty")} -

    - )} - - {notifications.map(notification => ( - - setNotifications(prev => - prev.map(n => - n.id === id ? { ...n, state: "SEEN" } : n - ) - ) - } - /> - ))} -
+
+ {notifications.length === 0 ? ( +

+ {t("panel.header.notifications.empty")} +

+ ) : ( +
+ {virtualizer.getVirtualItems().map(virtualItem => { + const notification = notifications[virtualItem.index]; + return ( +
+ +
+ ); + })} +
+ )}
clearInterval(interval); }, [clusters]); diff --git a/src/pages/panel/cron-jobs/index.tsx b/src/pages/panel/cron-jobs/index.tsx index c7f9b70..ed781a8 100644 --- a/src/pages/panel/cron-jobs/index.tsx +++ b/src/pages/panel/cron-jobs/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -88,7 +89,7 @@ export default function CronJobsPage() { loadCronJobs(); loadNamespaces(); - const interval = window.setInterval(loadCronJobs, 3000); + const interval = window.setInterval(loadCronJobs, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/daemon-sets/index.tsx b/src/pages/panel/daemon-sets/index.tsx index 73fbe22..c497ad9 100644 --- a/src/pages/panel/daemon-sets/index.tsx +++ b/src/pages/panel/daemon-sets/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -85,7 +86,7 @@ export default function DaemonSetsPage() { loadDaemonSets(); loadNamespaces(); - const interval = window.setInterval(loadDaemonSets, 3000); + const interval = window.setInterval(loadDaemonSets, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/deployments/index.tsx b/src/pages/panel/deployments/index.tsx index 1f08031..eda9878 100644 --- a/src/pages/panel/deployments/index.tsx +++ b/src/pages/panel/deployments/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -85,7 +86,7 @@ export default function DeploymentsPage() { loadDeployments(); loadNamespaces(); - const interval = window.setInterval(loadDeployments, 3000); + const interval = window.setInterval(loadDeployments, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/events/index.tsx b/src/pages/panel/events/index.tsx index 656936b..49d7344 100644 --- a/src/pages/panel/events/index.tsx +++ b/src/pages/panel/events/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import PanelPage from "@/layouts/panel" import {useEffect, useState} from "react"; import EventService, {type Event} from "@/api/services/event-service.ts"; @@ -107,7 +108,7 @@ export default function EventsPage() { } loadEvents(); - const interval = setInterval(loadEvents, 3000); + const interval = setInterval(loadEvents, POLL_INTERVAL_MS); return () => clearInterval(interval); }, [startDate, endDate, limit, selectedValues, search]); diff --git a/src/pages/panel/namespaces/index.tsx b/src/pages/panel/namespaces/index.tsx index d1899a0..b40e0eb 100644 --- a/src/pages/panel/namespaces/index.tsx +++ b/src/pages/panel/namespaces/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -55,7 +56,7 @@ export default function NamespacesPage() { }; useEffect(() => { loadNamespaces(); - const interval = window.setInterval(loadNamespaces, 3000); + const interval = window.setInterval(loadNamespaces, POLL_INTERVAL_MS); return () => clearInterval(interval); }, []); if (!isLoading && namespaces.length === 0) { diff --git a/src/pages/panel/nodes/index.tsx b/src/pages/panel/nodes/index.tsx index b1d2937..52e0116 100644 --- a/src/pages/panel/nodes/index.tsx +++ b/src/pages/panel/nodes/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -81,7 +82,7 @@ export default function NodesPage() { } } loadNodes(); - const interval = window.setInterval(loadNodes, 3000); + const interval = window.setInterval(loadNodes, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/overview/activity.tsx b/src/pages/panel/overview/activity.tsx index 2dbea5b..d200420 100644 --- a/src/pages/panel/overview/activity.tsx +++ b/src/pages/panel/overview/activity.tsx @@ -1,5 +1,6 @@ "use client"; +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {startTransition, useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import { Bar, BarChart, CartesianGrid, Rectangle, XAxis } from "recharts"; @@ -142,7 +143,7 @@ export default function OverviewActivityBox() { loadEvents(); - const interval = setInterval(loadEvents, 3000); + const interval = setInterval(loadEvents, POLL_INTERVAL_MS); return () => clearInterval(interval); }, []); diff --git a/src/pages/panel/overview/index.tsx b/src/pages/panel/overview/index.tsx index f8e6c0c..ab3c56f 100644 --- a/src/pages/panel/overview/index.tsx +++ b/src/pages/panel/overview/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import PanelPage from "@/layouts/panel" import OverviewStatusBox from "@/pages/panel/overview/status.tsx"; import OverviewWorkloadBox from "@/pages/panel/overview/workload.tsx"; @@ -21,7 +22,7 @@ export default function OverviewPage() { } } loadNodes(); - const interval = window.setInterval(loadNodes, 3000); + const interval = window.setInterval(loadNodes, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/overview/news.tsx b/src/pages/panel/overview/news.tsx index df5cb23..9015cb5 100644 --- a/src/pages/panel/overview/news.tsx +++ b/src/pages/panel/overview/news.tsx @@ -1,7 +1,9 @@ "use client" -import {startTransition, useEffect, useState} from "react"; +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; +import {startTransition, useEffect, useRef, useState} from "react"; import {useTranslation} from "react-i18next"; +import {useVirtualizer} from "@tanstack/react-virtual"; import { Card, CardContent, @@ -36,6 +38,7 @@ const getNotificationIconAndColor = (type: Notification['type']) => { export default function OverviewNewsBox() { const {t} = useTranslation(); const [notifications, setNotifications] = useState([]); + const scrollRef = useRef(null); useEffect(() => { const fetchNotifications = async () => { @@ -48,10 +51,17 @@ export default function OverviewNewsBox() { }; fetchNotifications(); - const interval = setInterval(fetchNotifications, 3000); + const interval = setInterval(fetchNotifications, POLL_INTERVAL_MS); return () => clearInterval(interval); }, []); + const virtualizer = useVirtualizer({ + count: notifications.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 56, + overscan: 5, + }); + return ( @@ -63,31 +73,47 @@ export default function OverviewNewsBox() { {notifications.length > 0 ? (
+
+
+ {virtualizer.getVirtualItems().map(virtualItem => { + const notification = notifications[virtualItem.index]; + const { icon, color } = getNotificationIconAndColor(notification.type); + const age = Date.now() - notification.created_at; -
- {notifications.map(notification => { - const { icon, color } = getNotificationIconAndColor(notification.type); - const age = Date.now() - notification.created_at; - - return ( -
-
- {icon} -
-
-

- {notification.title} -

-

- {notification.description} -

-
-
- + return ( +
+
+
+ {icon} +
+
+

+ {notification.title} +

+

+ {notification.description} +

+
+
+ +
+
-
- ); - })} + ); + })} +
{ clearInterval(interval); }; diff --git a/src/pages/panel/replica-sets/index.tsx b/src/pages/panel/replica-sets/index.tsx index 3d7547b..9d0954e 100644 --- a/src/pages/panel/replica-sets/index.tsx +++ b/src/pages/panel/replica-sets/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -90,7 +91,7 @@ export default function ReplicaSetsPage() { loadReplicaSets(); loadNamespaces(); - const interval = window.setInterval(loadReplicaSets, 3000); + const interval = window.setInterval(loadReplicaSets, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/services/index.tsx b/src/pages/panel/services/index.tsx index 7e70412..2e02266 100644 --- a/src/pages/panel/services/index.tsx +++ b/src/pages/panel/services/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -87,7 +88,7 @@ export default function ServicesPage() { loadServices(); loadNamespaces(); - const interval = window.setInterval(loadServices, 3000); + const interval = window.setInterval(loadServices, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/pages/panel/stateful-sets/index.tsx b/src/pages/panel/stateful-sets/index.tsx index cdab708..14d9562 100644 --- a/src/pages/panel/stateful-sets/index.tsx +++ b/src/pages/panel/stateful-sets/index.tsx @@ -1,3 +1,4 @@ +import {POLL_INTERVAL_MS} from "@/lib/constants.ts"; import {useEffect, useState} from "react"; import {DataTable} from "@/components/table"; import PanelPage from "@/layouts/panel"; @@ -85,7 +86,7 @@ export default function StatefulSetsPage() { loadStatefulSets(); loadNamespaces(); - const interval = window.setInterval(loadStatefulSets, 3000); + const interval = window.setInterval(loadStatefulSets, POLL_INTERVAL_MS); return () => { clearInterval(interval); }; diff --git a/src/routes/components/login-auth-guard.tsx b/src/routes/components/login-auth-guard.tsx index 9968ec6..c6dc3d5 100644 --- a/src/routes/components/login-auth-guard.tsx +++ b/src/routes/components/login-auth-guard.tsx @@ -1,6 +1,6 @@ import {useEffect} from "react"; import {useRouter} from "../hooks"; -import useUserStore, {useUserToken} from "@/store/user-store"; +import {useHasUserHydrated, useUserToken} from "@/store/user-store"; type Props = { children: React.ReactNode; @@ -9,7 +9,7 @@ export default function LoginAuthGuard({children}: Props) { const router = useRouter(); const {authentication_token} = useUserToken(); - const hasHydrated = useUserStore.persist.hasHydrated(); + const hasHydrated = useHasUserHydrated(); useEffect(() => { if (hasHydrated && !authentication_token) { diff --git a/src/store/cookie-store.ts b/src/store/cookie-store.ts index 05cf5d2..bb21967 100644 --- a/src/store/cookie-store.ts +++ b/src/store/cookie-store.ts @@ -1,16 +1,12 @@ import type {StateStorage} from 'zustand/middleware'; import {getCookie, removeCookie, setCookie} from 'typescript-cookie'; -const cookiesStorage: StateStorage = { - getItem: (name: string) => { - return getCookie(name) ?? null; +export const cookieStorage = (expiry: number): StateStorage => ({ + getItem: (name) => getCookie(name) ?? null, + setItem: (name, value) => { + setCookie(name, value, {expires: expiry, secure: true, path: "/", sameSite: "lax"}); }, - setItem: (name: string, value: string) => { - setCookie(name, value, {expires: 1, secure: true}); + removeItem: (name) => { + removeCookie(name, {path: "/", sameSite: "lax"}); }, - removeItem: (name: string) => { - removeCookie(name); - } -} - -export default cookiesStorage; \ No newline at end of file +}); diff --git a/src/store/user-store.ts b/src/store/user-store.ts index 8ddb988..f2e1ba1 100644 --- a/src/store/user-store.ts +++ b/src/store/user-store.ts @@ -1,12 +1,16 @@ +import {useMemo} from "react"; import {useMutation} from "@tanstack/react-query"; import {create} from "zustand"; import {createJSONStorage, persist} from "zustand/middleware"; import userService, {type LoginRequest} from "@/api/services/user-service"; -import cookiesStorage from "@/store/cookie-store.ts"; +import {cookieStorage} from "@/store/cookie-store.ts"; import {toast} from "sonner"; import {useTranslation} from "react-i18next"; import {useRouter} from "@/routes/hooks"; +const AUTH_COOKIE_EXPIRY = 1; +const REFRESH_COOKIE_EXPIRY = 30; + export interface UserToken { authentication_token?: string; refresh_token?: string; @@ -17,55 +21,122 @@ export interface UserInformation { name?: string; } -type UserStore = { - token: UserToken; +type AuthStore = { + authentication_token?: string; information: UserInformation; + actions: { + setAuthenticationToken: (token?: string) => void; + clearAuthenticationToken: () => void; + setInformation: (info: UserInformation) => void; + clearInformation: () => void; + }; +}; +type RefreshStore = { + refresh_token?: string; actions: { - setUserToken: (token: UserToken) => void; - clearUserToken: () => void; - setUserInformation: (token: UserInformation) => void; - clearUserInformation: () => void; + setRefreshToken: (token?: string) => void; + clearRefreshToken: () => void; }; }; -const useUserStore = create()( +const useAuthStore = create()( persist( (set) => ({ - token: {}, + authentication_token: undefined, information: {}, - actions: { - setUserToken: (token) => { - set({ token }); - }, - clearUserToken() { - set({ token: {} }); - }, - - setUserInformation: (info) => { - set({ information: info }); - }, - clearUserInformation() { - set({ information: {} }); - }, + setAuthenticationToken: (token) => set({authentication_token: token}), + clearAuthenticationToken: () => set({authentication_token: undefined}), + setInformation: (info) => set({information: info}), + clearInformation: () => set({information: {}}), }, }), { name: "user", - storage: createJSONStorage(() => cookiesStorage), - partialize: (state) => ({ - token: state.token, - information: state.information, + storage: createJSONStorage(() => cookieStorage(AUTH_COOKIE_EXPIRY)), + partialize: (s) => ({ + authentication_token: s.authentication_token, + information: s.information, }), }, ), ); -export const useUserToken = () => useUserStore((state) => state.token); -export const useUserInformation = () => - useUserStore((state) => state.information); -export const useUserActions = () => useUserStore((state) => state.actions); +const useRefreshStore = create()( + persist( + (set) => ({ + refresh_token: undefined, + actions: { + setRefreshToken: (token) => set({refresh_token: token}), + clearRefreshToken: () => set({refresh_token: undefined}), + }, + }), + { + name: "user-refresh", + storage: createJSONStorage(() => cookieStorage(REFRESH_COOKIE_EXPIRY)), + partialize: (s) => ({refresh_token: s.refresh_token}), + }, + ), +); + +export const useUserToken = (): UserToken => { + const authentication_token = useAuthStore((s) => s.authentication_token); + const refresh_token = useRefreshStore((s) => s.refresh_token); + return {authentication_token, refresh_token}; +}; + +export const useUserInformation = () => useAuthStore((s) => s.information); + +export const useUserActions = () => { + const authActions = useAuthStore((s) => s.actions); + const refreshActions = useRefreshStore((s) => s.actions); + return useMemo( + () => ({ + setUserToken: (token: UserToken) => { + authActions.setAuthenticationToken(token.authentication_token); + refreshActions.setRefreshToken(token.refresh_token); + }, + clearUserToken: () => { + authActions.clearAuthenticationToken(); + refreshActions.clearRefreshToken(); + }, + setUserInformation: authActions.setInformation, + clearUserInformation: authActions.clearInformation, + }), + [authActions, refreshActions], + ); +}; + +export const useHasUserHydrated = () => { + const auth = useAuthStore.persist.hasHydrated(); + const refresh = useRefreshStore.persist.hasHydrated(); + return auth && refresh; +}; + +const userStore = { + getState: () => { + const auth = useAuthStore.getState(); + const refresh = useRefreshStore.getState(); + return { + token: { + authentication_token: auth.authentication_token, + refresh_token: refresh.refresh_token, + } as UserToken, + information: auth.information, + actions: { + setUserToken: (token: UserToken) => { + auth.actions.setAuthenticationToken(token.authentication_token); + refresh.actions.setRefreshToken(token.refresh_token); + }, + clearUserToken: () => { + auth.actions.clearAuthenticationToken(); + refresh.actions.clearRefreshToken(); + }, + }, + }; + }, +}; export const useLogin = () => { const { t } = useTranslation(); @@ -115,4 +186,4 @@ export const useLogout = () => { }; }; -export default useUserStore; \ No newline at end of file +export default userStore; diff --git a/tsconfig.app.json b/tsconfig.app.json index 6508322..2eb2a21 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -27,7 +27,8 @@ "baseUrl": "./src", "paths": { "@/*": ["*"] - } + }, + "ignoreDeprecations": "6.0" }, "include": ["src"] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 7802d96..8e0613d 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -25,7 +25,8 @@ "baseUrl": "./src", "paths": { "@/*": ["*"] - } + }, + "ignoreDeprecations": "6.0" }, "include": ["vite.config.ts"] }