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}