From 8d1de769c54835f663b82d39d7f3bdf1bbb3adf7 Mon Sep 17 00:00:00 2001 From: itoshi Date: Thu, 9 Apr 2026 18:50:40 +0300 Subject: [PATCH] feat: add snapshot memory and blocked create button --- .env | 6 +- .../Snapshot/SnapshotCreatePage/index.tsx | 3 +- src/pages/Snapshot/SnapshotListPage/index.tsx | 213 +++++++++++++++--- src/shared/store/SnapshotStore/index.ts | 24 +- src/widgets/CreateButton/index.tsx | 4 +- 5 files changed, 210 insertions(+), 40 deletions(-) diff --git a/.env b/.env index a5c21cf..c8ded82 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ -# # VITE_API_URL='https://wn.st.unprism.ru' -# # VITE_REACT_APP ='https://wn.st.unprism.ru/' -# # VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' +# VITE_API_URL='https://wn.st.unprism.ru' +# VITE_REACT_APP ='https://wn.st.unprism.ru/' +# VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' # VITE_NEED_AUTH='true' VITE_API_URL='https://wn.krbl.ru' VITE_REACT_APP ='https://wn.krbl.ru/' diff --git a/src/pages/Snapshot/SnapshotCreatePage/index.tsx b/src/pages/Snapshot/SnapshotCreatePage/index.tsx index a02c9b6..d3312e1 100644 --- a/src/pages/Snapshot/SnapshotCreatePage/index.tsx +++ b/src/pages/Snapshot/SnapshotCreatePage/index.tsx @@ -8,7 +8,7 @@ import { toast } from "react-toastify"; import { runInAction } from "mobx"; export const SnapshotCreatePage = observer(() => { - const { createSnapshot, getSnapshotStatus, snapshotStatus } = snapshotStore; + const { createSnapshot, getSnapshotStatus, getStorageInfo, snapshotStatus } = snapshotStore; const navigate = useNavigate(); const [name, setName] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -59,6 +59,7 @@ export const SnapshotCreatePage = observer(() => { snapshotStore.snapshotStatus = null; }); + await getStorageInfo(); navigate(-1); } } catch (error) { diff --git a/src/pages/Snapshot/SnapshotListPage/index.tsx b/src/pages/Snapshot/SnapshotListPage/index.tsx index 0664f7d..7e64a3e 100644 --- a/src/pages/Snapshot/SnapshotListPage/index.tsx +++ b/src/pages/Snapshot/SnapshotListPage/index.tsx @@ -5,13 +5,35 @@ import { useEffect, useState, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { DatabaseBackup, Trash2 } from "lucide-react"; import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets"; -import { Box, CircularProgress } from "@mui/material"; +import { Alert, Box, CircularProgress } from "@mui/material"; + +const LOW_STORAGE_THRESHOLD_GB = 10; + +const SEGMENT_COLORS = [ + "#FF3B30", + "#FF9500", + "#FFCC00", + "#8E8E93", + "#AEAEB2", + "#34C759", + "#007AFF", + "#5856D6", + "#AF52DE", + "#FF2D55", +]; export const SnapshotListPage = observer(() => { - const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } = - snapshotStore; + const { + snapshots, + getSnapshots, + deleteSnapshot, + restoreSnapshot, + storageInfo, + getStorageInfo, + } = snapshotStore; const canWriteDevices = authStore.canWrite("devices"); - const canCreateSnapshot = authStore.hasRole("snapshot_create") && canWriteDevices; + const canCreateSnapshot = + authStore.hasRole("snapshot_create") && canWriteDevices; const canManageSnapshots = authStore.canWrite("snapshot") && canWriteDevices; const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -25,10 +47,17 @@ export const SnapshotListPage = observer(() => { pageSize: 50, }); + const availableGB = storageInfo ? storageInfo.available_memory : null; + const totalGB = storageInfo ? storageInfo.all_memory : null; + const usedGB = + totalGB !== null && availableGB !== null ? totalGB - availableGB : null; + const isLowStorage = + availableGB !== null && availableGB < LOW_STORAGE_THRESHOLD_GB; + useEffect(() => { const fetchSnapshots = async () => { setIsLoading(true); - await getSnapshots(); + await Promise.all([getSnapshots(), getStorageInfo()]); setIsLoading(false); }; fetchSnapshots(); @@ -61,33 +90,49 @@ export const SnapshotListPage = observer(() => { return
{params.value ? params.value : "-"}
; }, }, - ...(canManageSnapshots ? [{ - field: "actions", - headerName: "Действия", - width: 300, - headerAlign: "center" as const, - sortable: false, - renderCell: (params: GridRenderCellParams) => ( -
- - -
- ), - }] : []), + { + field: "occupied_memory", + headerName: "Размер", + width: 120, + renderCell: (params: GridRenderCellParams) => { + return ( +
+ {params.value != null ? `${params.value.toFixed(1)} ГБ` : "-"} +
+ ); + }, + }, + ...(canManageSnapshots + ? [ + { + field: "actions", + headerName: "Действия", + width: 300, + headerAlign: "center" as const, + sortable: false, + renderCell: (params: GridRenderCellParams) => ( +
+ + +
+ ), + }, + ] + : []), ]; const rows = useMemo(() => { @@ -97,16 +142,25 @@ export const SnapshotListPage = observer(() => { (snapshot) => !query || (snapshot.Name ?? "").toLowerCase().includes(query) || - (snapshots.find((s) => s.ID === snapshot.ParentID)?.Name ?? "").toLowerCase().includes(query) + (snapshots.find((s) => s.ID === snapshot.ParentID)?.Name ?? "") + .toLowerCase() + .includes(query), ) .map((snapshot) => ({ id: snapshot.ID, name: snapshot.Name, parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name, created_at: formatCreationTime(snapshot.CreationTime), + occupied_memory: snapshot.occupied_memory, })); }, [snapshots, searchQuery]); + const snapshotsGB = rows.reduce( + (sum, row) => sum + (row.occupied_memory ?? 0), + 0, + ); + const systemGB = usedGB !== null ? Math.max(0, usedGB - snapshotsGB) : null; + return ( <>
@@ -114,9 +168,100 @@ export const SnapshotListPage = observer(() => {

Экспорт Медиа

{canCreateSnapshot && ( - + )}
+ {usedGB !== null && totalGB !== null && ( +
+
+ Хранилище + + Используется: {usedGB.toFixed(2)} ГБ из {totalGB.toFixed(0)} ГБ + +
+ +
+ {rows.map((row, i) => { + const pct = + row.occupied_memory != null && totalGB > 0 + ? (row.occupied_memory / totalGB) * 100 + : 0; + if (pct <= 0) return null; + return ( +
+ ); + })} + {systemGB !== null && systemGB > 0 && totalGB > 0 && ( +
+ )} +
+ +
+ {rows.map((row, i) => { + if (row.occupied_memory == null || row.occupied_memory <= 0) + return null; + return ( +
+ + {row.name} +
+ ); + })} + {systemGB !== null && systemGB > 0 && ( +
+ + Системные данные +
+ )} + {availableGB !== null && availableGB > 0 && ( +
+ + Свободно +
+ )} +
+
+ )} + + {isLowStorage && ( + + Недостаточно места на диске! Осталось {availableGB?.toFixed(1)} ГБ + из {totalGB?.toFixed(0)} ГБ. Создание новых экспортов заблокировано. + Удалите ненужные экспорты для освобождения места или обратитесь к + администратору сервера. + + )} + { + const snapshot = this.snapshots.find((s) => s.ID === id); await authInstance.delete(`/snapshots/${id}`); runInAction(() => { - this.snapshots = this.snapshots.filter((snapshot) => snapshot.ID !== id); + this.snapshots = this.snapshots.filter((s) => s.ID !== id); + if (this.storageInfo && snapshot?.occupied_memory) { + this.storageInfo = { + ...this.storageInfo, + available_memory: this.storageInfo.available_memory + snapshot.occupied_memory, + }; + } }); }; @@ -299,6 +313,14 @@ class SnapshotStore { this.snapshotStatus = response.data; }); }; + + getStorageInfo = async () => { + const response = await authInstance.get(`/snapshots/storage`); + + runInAction(() => { + this.storageInfo = response.data; + }); + }; } export const snapshotStore = new SnapshotStore(); diff --git a/src/widgets/CreateButton/index.tsx b/src/widgets/CreateButton/index.tsx index b3b5011..09d3e52 100644 --- a/src/widgets/CreateButton/index.tsx +++ b/src/widgets/CreateButton/index.tsx @@ -5,9 +5,10 @@ import { useNavigate } from "react-router-dom"; interface CreateButtonProps { label: string; path: string; + disabled?: boolean; } -export const CreateButton = ({ label, path }: CreateButtonProps) => { +export const CreateButton = ({ label, path, disabled }: CreateButtonProps) => { const navigate = useNavigate(); return ( @@ -18,6 +19,7 @@ export const CreateButton = ({ label, path }: CreateButtonProps) => { navigate(path); }} startIcon={} + disabled={disabled} > {label}