Compare commits
2 Commits
4b02c6e9d3
...
fbf8232ce3
| Author | SHA1 | Date | |
|---|---|---|---|
| fbf8232ce3 | |||
| 8d1de769c5 |
6
.env
6
.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/'
|
||||
|
||||
@@ -99,7 +99,7 @@ export const LinkedItems = <
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
Привязанные станции
|
||||
Привязанные остановки
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
@@ -499,7 +499,7 @@ const LinkedItemsContentsInner = <
|
||||
|
||||
{linkedItems.length === 0 && !isLoading && (
|
||||
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||
Станции не найдены
|
||||
Остановки не найдены
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Widgets() {
|
||||
justifyContent="center"
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||
Станция
|
||||
Остановка
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,7 +90,21 @@ export const SnapshotListPage = observer(() => {
|
||||
return <div>{params.value ? params.value : "-"}</div>;
|
||||
},
|
||||
},
|
||||
...(canManageSnapshots ? [{
|
||||
{
|
||||
field: "occupied_memory",
|
||||
headerName: "Размер",
|
||||
width: 120,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div>
|
||||
{params.value != null ? `${params.value.toFixed(1)} ГБ` : "-"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(canManageSnapshots
|
||||
? [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 300,
|
||||
@@ -87,7 +130,9 @@ export const SnapshotListPage = observer(() => {
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}] : []),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div style={{ width: "100%" }}>
|
||||
@@ -114,9 +168,100 @@ export const SnapshotListPage = observer(() => {
|
||||
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
||||
|
||||
{canCreateSnapshot && (
|
||||
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
||||
<CreateButton
|
||||
label="Создать экспорт медиа"
|
||||
path="/snapshot/create"
|
||||
disabled={isLowStorage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{usedGB !== null && totalGB !== null && (
|
||||
<div className="bg-white rounded-2xl p-5 mb-6 shadow-sm border border-gray-100">
|
||||
<div className="flex items-baseline gap-3 mb-3">
|
||||
<span className="text-lg font-semibold">Хранилище</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
Используется: {usedGB.toFixed(2)} ГБ из {totalGB.toFixed(0)} ГБ
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full h-3 rounded-lg overflow-hidden bg-gray-100">
|
||||
{rows.map((row, i) => {
|
||||
const pct =
|
||||
row.occupied_memory != null && totalGB > 0
|
||||
? (row.occupied_memory / totalGB) * 100
|
||||
: 0;
|
||||
if (pct <= 0) return null;
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
backgroundColor:
|
||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
||||
}}
|
||||
title={`${row.name}: ${row.occupied_memory?.toFixed(1)} ГБ`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{systemGB !== null && systemGB > 0 && totalGB > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: `${(systemGB / totalGB) * 100}%`,
|
||||
backgroundColor: "#C7C7CC",
|
||||
}}
|
||||
title={`Системные данные: ${systemGB.toFixed(1)} ГБ`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3">
|
||||
{rows.map((row, i) => {
|
||||
if (row.occupied_memory == null || row.occupied_memory <= 0)
|
||||
return null;
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-700"
|
||||
>
|
||||
<span
|
||||
className="inline-block w-2.5 h-2.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
||||
}}
|
||||
/>
|
||||
{row.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{systemGB !== null && systemGB > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-700">
|
||||
<span
|
||||
className="inline-block w-2.5 h-2.5 rounded-full"
|
||||
style={{ backgroundColor: "#C7C7CC" }}
|
||||
/>
|
||||
Системные данные
|
||||
</div>
|
||||
)}
|
||||
{availableGB !== null && availableGB > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-700">
|
||||
<span className="inline-block w-2.5 h-2.5 rounded-full bg-gray-100" />
|
||||
Свободно
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLowStorage && (
|
||||
<Alert severity="warning" className="mb-4">
|
||||
Недостаточно места на диске! Осталось {availableGB?.toFixed(1)} ГБ
|
||||
из {totalGB?.toFixed(0)} ГБ. Создание новых экспортов заблокировано.
|
||||
Удалите ненужные экспорты для освобождения места или обратитесь к
|
||||
администратору сервера.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||
|
||||
<DataGrid
|
||||
|
||||
@@ -73,7 +73,7 @@ export const StationCreatePage = observer(() => {
|
||||
navigate("/station");
|
||||
} catch (error) {
|
||||
console.error("Error creating station:", error);
|
||||
toast.error("Ошибка при создании станции");
|
||||
toast.error("Ошибка при создании остановки");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export const StationEditPage = observer(() => {
|
||||
toast.success("Остановка успешно обновлена");
|
||||
} catch (error) {
|
||||
console.error("Error updating station:", error);
|
||||
toast.error("Ошибка при обновлении станции");
|
||||
toast.error("Ошибка при обновлении остановки");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export const StationEditPage = observer(() => {
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных станции..." />
|
||||
<LoadingSpinner message="Загрузка данных остановки..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export const StationListPage = observer(() => {
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Станции</h1>
|
||||
<h1 className="text-2xl">Остановки</h1>
|
||||
{canWriteStations && (
|
||||
<CreateButton label="Создать остановки" path="/station/create" />
|
||||
)}
|
||||
@@ -222,7 +222,7 @@ export const StationListPage = observer(() => {
|
||||
slots={{
|
||||
noRowsOverlay: () => (
|
||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||
{isLoading ? <CircularProgress size={20} /> : "Нет станций"}
|
||||
{isLoading ? <CircularProgress size={20} /> : "Нет остановок"}
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const StationPreviewPage = observer(() => {
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных станции..." />
|
||||
<LoadingSpinner message="Загрузка данных остановки..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ class RouteStore {
|
||||
saveRouteStations = async (routeId: number, stationId: number) => {
|
||||
const { language } = languageStore;
|
||||
|
||||
// Получаем актуальные данные станции с сервера
|
||||
const stationResponse = await languageInstance(language).get(
|
||||
`/station/${stationId}`
|
||||
);
|
||||
@@ -108,7 +107,6 @@ class RouteStore {
|
||||
(station) => station.id === stationId
|
||||
);
|
||||
|
||||
// Формируем данные для отправки: все поля станции + отредактированные offset
|
||||
const dataToSend: any = {
|
||||
station_id: stationId,
|
||||
offset_x: editedStationData?.offset_x ?? fullStationData.offset_x ?? 0,
|
||||
|
||||
@@ -23,6 +23,7 @@ type Snapshot = {
|
||||
Name: string;
|
||||
ParentID: string;
|
||||
CreationTime: string;
|
||||
occupied_memory: number;
|
||||
};
|
||||
|
||||
type SnapshotStatus = {
|
||||
@@ -32,11 +33,17 @@ type SnapshotStatus = {
|
||||
Error: string;
|
||||
};
|
||||
|
||||
type StorageInfo = {
|
||||
available_memory: number;
|
||||
all_memory: number;
|
||||
};
|
||||
|
||||
class SnapshotStore {
|
||||
snapshots: Snapshot[] = [];
|
||||
snapshot: Snapshot | null = null;
|
||||
lastRequestId: string | null = null;
|
||||
snapshotStatus: SnapshotStatus | null = null;
|
||||
storageInfo: StorageInfo | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
@@ -259,10 +266,17 @@ class SnapshotStore {
|
||||
};
|
||||
|
||||
deleteSnapshot = async (id: string) => {
|
||||
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();
|
||||
|
||||
@@ -555,7 +555,6 @@ class StationsStore {
|
||||
) => {
|
||||
const { language } = languageStore;
|
||||
|
||||
// Получаем данные станции для текущего языка
|
||||
const response = await languageInstance(language).get(`/station/${id}`);
|
||||
const stationData = response.data as Station;
|
||||
|
||||
|
||||
@@ -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={<Plus size={20} />}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
@@ -117,7 +117,7 @@ export const EditStationTransfersModal = observer(
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Станции / Редактировать пересадки
|
||||
Остановки / Редактировать пересадки
|
||||
</Typography>
|
||||
<Typography variant="h6">Редактирование пересадок</Typography>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user