Compare commits

..

5 Commits

Author SHA1 Message Date
34ba3c1db0 Add Dockerfile and Makefile for containerization and build automation
- Created a Dockerfile with a multi-stage build process to containerize the application.
- Added Makefile for managing build, export, and cleanup tasks.
2025-07-29 17:39:21 +03:00
4f038551a2 fix: Fix problems and bugs 2025-07-28 08:18:21 +03:00
470a58a3fa fix: Fix panorama + route scale data 2025-07-26 11:48:41 +03:00
89d7fc2748 feat: Add scale on group click, add cache for map entities, fix map preview loading 2025-07-15 05:29:27 +03:00
97f95fc394 feat: Group map entities + delete useless logs 2025-07-13 20:56:25 +03:00
27 changed files with 1156 additions and 444 deletions

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# Stage 1: Build the application
FROM node:20-alpine AS build
# Set working directory
WORKDIR /app
# Copy package.json and yarn.lock
COPY package.json yarn.lock ./
# Install dependencies
RUN yarn install --frozen-lockfile
# Copy the rest of the application code
COPY . .
# Build the application
RUN yarn build
# Stage 2: Serve the application with Nginx
FROM nginx:alpine
# Copy the built application from the build stage
COPY --from=build /app/dist /usr/share/nginx/html
# Copy nginx configuration (optional, can be added later if needed)
# COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]

38
Makefile Normal file
View File

@ -0,0 +1,38 @@
# Variables
IMAGE_NAME = white-nights-admin-panel
IMAGE_TAG = latest
FULL_IMAGE_NAME = $(IMAGE_NAME):$(IMAGE_TAG)
ARCHIVE_NAME = white-nights-admin-panel-image.zip
# Default target
.PHONY: help
help:
@echo "Available commands:"
@echo " make build-image - Build Docker image"
@echo " make export-image - Build Docker image and export it to a zip archive"
@echo " make clean - Remove Docker image and zip archive"
@echo " make help - Show this help message"
# Build Docker image
.PHONY: build-image
build-image:
@echo "Building Docker image: $(FULL_IMAGE_NAME)"
docker build -t $(FULL_IMAGE_NAME) .
# Export Docker image to zip archive
.PHONY: export-image
export-image: build-image
@echo "Exporting Docker image to $(ARCHIVE_NAME)"
docker save $(FULL_IMAGE_NAME) | gzip > $(ARCHIVE_NAME)
@echo "Image exported successfully to $(ARCHIVE_NAME)"
# Clean up
.PHONY: clean
clean:
@echo "Removing Docker image and zip archive"
-docker rmi $(FULL_IMAGE_NAME) 2>/dev/null || true
-rm -f $(ARCHIVE_NAME) 2>/dev/null || true
@echo "Clean up completed"
# Default target when no arguments provided
.DEFAULT_GOAL := help

File diff suppressed because it is too large Load Diff

View File

@ -45,10 +45,10 @@ export const MediaEditPage = observer(() => {
if (id) { if (id) {
mediaStore.getOneMedia(id); mediaStore.getOneMedia(id);
} }
console.log(newFile);
console.log(uploadDialogOpen);
}, [id]); }, [id]);
useEffect(() => {}, [newFile, uploadDialogOpen]);
useEffect(() => { useEffect(() => {
if (media) { if (media) {
setMediaName(media.media_name); setMediaName(media.media_name);

View File

@ -140,9 +140,7 @@ export const LinkedItemsContents = <
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {}, [error]);
console.log(error);
}, [error]);
const parentResource = "route"; const parentResource = "route";
const childResource = "station"; const childResource = "station";
@ -227,7 +225,7 @@ export const LinkedItemsContents = <
if (type === "edit") { if (type === "edit") {
setError(null); setError(null);
authInstance authInstance
.get(`/${childResource}/`) .get(`/${childResource}`)
.then((response) => { .then((response) => {
setAllItems(response?.data || []); setAllItems(response?.data || []);
}) })

View File

@ -1,7 +1,7 @@
export const UP_SCALE = 30000; export const UP_SCALE = 10000;
export const PATH_WIDTH = 15; export const PATH_WIDTH = 5;
export const STATION_RADIUS = 20; export const STATION_RADIUS = 8;
export const STATION_OUTLINE_WIDTH = 10; export const STATION_OUTLINE_WIDTH = 4;
export const SIGHT_SIZE = 40; export const SIGHT_SIZE = 40;
export const SCALE_FACTOR = 50; export const SCALE_FACTOR = 50;

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { useTransform } from "./TransformContext"; import { useTransform } from "./TransformContext";
import { coordinatesToLocal, localToCoordinates } from "./utils"; import { coordinatesToLocal, localToCoordinates } from "./utils";
import { SCALE_FACTOR } from "./Constants"; import { SCALE_FACTOR } from "./Constants";
import { toast } from "react-toastify";
export function RightSidebar() { export function RightSidebar() {
const { const {
@ -360,8 +361,14 @@ export function RightSidebar() {
variant="contained" variant="contained"
color="secondary" color="secondary"
sx={{ mt: 2 }} sx={{ mt: 2 }}
onClick={() => { onClick={async () => {
saveChanges(); try {
await saveChanges();
toast.success("Изменения сохранены");
} catch (error) {
console.error(error);
toast.error("Ошибка при сохранении изменений");
}
}} }}
> >
Сохранить изменения Сохранить изменения

View File

@ -82,11 +82,7 @@ export const Sight = ({ sight, id }: Readonly<SightProps>) => {
Assets.load("/SightIcon.png").then(setTexture); Assets.load("/SightIcon.png").then(setTexture);
}, []); }, []);
useEffect(() => { useEffect(() => {}, [id, sight.latitude, sight.longitude]);
console.log(
`Rendering Sight ${id + 1} at [${sight.latitude}, ${sight.longitude}]`
);
}, [id, sight.latitude, sight.longitude]);
if (!sight) { if (!sight) {
console.error("sight is null"); console.error("sight is null");

View File

@ -57,8 +57,8 @@ export function Widgets() {
mb: 1, mb: 1,
}} }}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: "flex", gap: 0.5 }}>
<Landmark size={16} /> <Landmark size={16} className="shrink-0" />
<Typography <Typography
variant="subtitle2" variant="subtitle2"
sx={{ color: "#fff", fontWeight: "bold" }} sx={{ color: "#fff", fontWeight: "bold" }}

View File

@ -26,6 +26,7 @@ import { Sight } from "./Sight";
import { SightData } from "./types"; import { SightData } from "./types";
import { Station } from "./Station"; import { Station } from "./Station";
import { UP_SCALE } from "./Constants"; import { UP_SCALE } from "./Constants";
import CircularProgress from "@mui/material/CircularProgress";
extend({ extend({
Container, Container,
@ -36,13 +37,27 @@ extend({
Text, Text,
}); });
const Loading = () => {
const { isRouteLoading, isStationLoading, isSightLoading } = useMapData();
if (isRouteLoading || isStationLoading || isSightLoading) {
return (
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
<CircularProgress />
</div>
);
}
return null;
};
export const RoutePreview = () => { export const RoutePreview = () => {
const { routeData, stationData, sightData } = useMapData();
return ( return (
<MapDataProvider> <MapDataProvider>
<TransformProvider> <TransformProvider>
<Stack direction="row" height="100vh" width="100vw" overflow="hidden"> <Stack direction="row" height="100vh" width="100vw" overflow="hidden">
<LanguageSwitcher /> {routeData && stationData && sightData ? <LanguageSwitcher /> : null}
<Loading />
<LeftSidebar /> <LeftSidebar />
<Stack direction="row" flex={1} position="relative" height="100%"> <Stack direction="row" flex={1} position="relative" height="100%">
<RouteMap /> <RouteMap />
@ -145,12 +160,12 @@ export const RouteMap = observer(() => {
]); ]);
if (!routeData || !stationData || !sightData) { if (!routeData || !stationData || !sightData) {
console.error("routeData, stationData or sightData is null"); return null;
return <div>Loading...</div>;
} }
return ( return (
<div style={{ width: "100%", height: "100%" }} ref={parentRef}> <div style={{ width: "100%", height: "100%" }} ref={parentRef}>
<LanguageSwitcher />
<Application resizeTo={parentRef} background="#fff"> <Application resizeTo={parentRef} background="#fff">
<InfiniteCanvas> <InfiniteCanvas>
<TravelPath points={points} /> <TravelPath points={points} />

View File

@ -132,7 +132,6 @@ export const SightListPage = observer(() => {
loading={isLoading} loading={isLoading}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => { onRowSelectionModelChange={(newSelection) => {
console.log(newSelection);
setIds(Array.from(newSelection.ids as unknown as number[])); setIds(Array.from(newSelection.ids as unknown as number[]));
}} }}
slots={{ slots={{

View File

@ -1,4 +1,4 @@
import { Button, Paper, TextField } from "@mui/material"; import { Button, TextField } from "@mui/material";
import { snapshotStore } from "@shared"; import { snapshotStore } from "@shared";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react"; import { ArrowLeft, Loader2, Save } from "lucide-react";
@ -20,7 +20,8 @@ export const SnapshotCreatePage = observer(() => {
}, [id]); }, [id]);
return ( return (
<Paper className="w-full h-full p-3 flex flex-col gap-10"> <div className="w-full h-[400px] flex justify-center items-center">
<div className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<button <button
className="flex items-center gap-2" className="flex items-center gap-2"
@ -51,11 +52,15 @@ export const SnapshotCreatePage = observer(() => {
await createSnapshot(name); await createSnapshot(name);
setIsLoading(false); setIsLoading(false);
toast.success("Снапшот успешно создан"); toast.success("Снапшот успешно создан");
navigate(-1);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error("Ошибка при создании снапшота");
} finally {
setIsLoading(false);
} }
}} }}
disabled={isLoading} disabled={isLoading || !name.trim()}
> >
{isLoading ? ( {isLoading ? (
<Loader2 size={20} className="animate-spin" /> <Loader2 size={20} className="animate-spin" />
@ -64,6 +69,7 @@ export const SnapshotCreatePage = observer(() => {
)} )}
</Button> </Button>
</div> </div>
</Paper> </div>
</div>
); );
}); });

View File

@ -117,12 +117,15 @@ export const SnapshotListPage = observer(() => {
<SnapshotRestore <SnapshotRestore
open={isRestoreModalOpen} open={isRestoreModalOpen}
loading={isLoading}
onDelete={async () => { onDelete={async () => {
setIsLoading(true);
if (rowId) { if (rowId) {
await restoreSnapshot(rowId); await restoreSnapshot(rowId);
} }
setIsRestoreModalOpen(false); setIsRestoreModalOpen(false);
setRowId(null); setRowId(null);
setIsLoading(false);
}} }}
onCancel={() => { onCancel={() => {
setIsRestoreModalOpen(false); setIsRestoreModalOpen(false);

View File

@ -1,3 +1,2 @@
export * from "./SnapshotListPage"; export * from "./SnapshotListPage";
export * from "./SnapshotCreatePage"; export * from "./SnapshotCreatePage";

View File

@ -93,9 +93,7 @@ export const LinkedSightsContents = <
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {}, [error]);
console.log(error);
}, [error]);
const parentResource = "station"; const parentResource = "station";
const childResource = "sight"; const childResource = "sight";
@ -178,7 +176,7 @@ export const LinkedSightsContents = <
if (type === "edit") { if (type === "edit") {
setError(null); setError(null);
authInstance authInstance
.get(`/${childResource}/`) .get(`/${childResource}`)
.then((response) => { .then((response) => {
setAllItems(response?.data || []); setAllItems(response?.data || []);
}) })

View File

@ -81,6 +81,7 @@ export const UploadMediaDialog = observer(
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>( const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
[] []
); );
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
useEffect(() => { useEffect(() => {
if (initialFile) { if (initialFile) {
@ -207,6 +208,7 @@ export const UploadMediaDialog = observer(
useEffect(() => { useEffect(() => {
if (mediaFile) { if (mediaFile) {
setMediaUrl(URL.createObjectURL(mediaFile as Blob)); setMediaUrl(URL.createObjectURL(mediaFile as Blob));
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
} }
}, [mediaFile]); }, [mediaFile]);
@ -326,8 +328,22 @@ export const UploadMediaDialog = observer(
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
height: "100%", height: "100%",
position: "relative",
}} }}
> >
{!isPreviewLoaded && mediaUrl && (
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1,
}}
>
<CircularProgress />
</Box>
)}
{mediaType == 2 && mediaUrl && ( {mediaType == 2 && mediaUrl && (
<video <video
src={mediaUrl} src={mediaUrl}
@ -336,10 +352,16 @@ export const UploadMediaDialog = observer(
loop loop
controls controls
style={{ maxWidth: "100%", maxHeight: "100%" }} style={{ maxWidth: "100%", maxHeight: "100%" }}
onLoadedData={() => setIsPreviewLoaded(true)}
onError={() => setIsPreviewLoaded(true)}
/> />
)} )}
{mediaType === 6 && mediaUrl && ( {mediaType === 6 && mediaUrl && (
<ModelViewer3D fileUrl={mediaUrl} height="100%" /> <ModelViewer3D
fileUrl={mediaUrl}
height="100%"
onLoad={() => setIsPreviewLoaded(true)}
/>
)} )}
{mediaType !== 6 && mediaType !== 2 && mediaUrl && ( {mediaType !== 6 && mediaType !== 2 && mediaUrl && (
<img <img
@ -349,6 +371,8 @@ export const UploadMediaDialog = observer(
height: "100%", height: "100%",
objectFit: "contain", objectFit: "contain",
}} }}
onLoad={() => setIsPreviewLoaded(true)}
onError={() => setIsPreviewLoaded(true)}
/> />
)} )}
</Paper> </Paper>
@ -370,9 +394,17 @@ export const UploadMediaDialog = observer(
) )
} }
onClick={handleSave} onClick={handleSave}
disabled={isLoading || (!mediaName && !mediaFilename)} disabled={
isLoading ||
(!mediaName && !mediaFilename) ||
!isPreviewLoaded
}
> >
{isLoading ? "Сохранение..." : "Сохранить"} {isLoading
? "Сохранение..."
: !isPreviewLoaded
? "Загрузка превью..."
: "Сохранить"}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@ -497,9 +497,7 @@ class EditSightStore {
media_name: media_name, media_name: media_name,
media_type: type, media_type: type,
}; };
} catch (error) { } catch (error) {}
console.log(error);
}
}; };
createLinkWithArticle = async (media: { createLinkWithArticle = async (media: {

View File

@ -82,11 +82,6 @@ class RouteStore {
}; };
setRouteStations = (routeId: number, stationId: number, data: any) => { setRouteStations = (routeId: number, stationId: number, data: any) => {
console.log(
this.routeStations[routeId],
stationId,
this.routeStations[routeId].find((station) => station.id === stationId)
);
this.routeStations[routeId] = this.routeStations[routeId]?.map((station) => this.routeStations[routeId] = this.routeStations[routeId]?.map((station) =>
station.id === stationId ? { ...station, ...data } : station station.id === stationId ? { ...station, ...data } : station
); );

View File

@ -97,15 +97,19 @@ class SightsStore {
city: number, city: number,
coordinates: { latitude: number; longitude: number } coordinates: { latitude: number; longitude: number }
) => { ) => {
const id = ( const response = await authInstance.post("/sight", {
await authInstance.post("/sight", {
name: this.createSight[languageStore.language].name, name: this.createSight[languageStore.language].name,
address: this.createSight[languageStore.language].address, address: this.createSight[languageStore.language].address,
city_id: city, city_id: city,
latitude: coordinates.latitude, latitude: coordinates.latitude,
longitude: coordinates.longitude, longitude: coordinates.longitude,
}) });
).data.id;
runInAction(() => {
this.sights.push(response.data);
});
const id = response.data.id;
const anotherLanguages = ["ru", "en", "zh"].filter( const anotherLanguages = ["ru", "en", "zh"].filter(
(language) => language !== languageStore.language (language) => language !== languageStore.language

View File

@ -1,6 +1,24 @@
import { authInstance } from "@shared"; import { authInstance } from "@shared";
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
// Импорт функции сброса кешей карты
// import { clearMapCaches } from "../../pages/MapPage";
import {
articlesStore,
cityStore,
countryStore,
carrierStore,
stationsStore,
sightsStore,
routeStore,
vehicleStore,
userStore,
mediaStore,
createSightStore,
editSightStore,
devicesStore,
authStore,
} from "@shared";
type Snapshot = { type Snapshot = {
ID: string; ID: string;
@ -17,6 +35,248 @@ class SnapshotStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
// Функция для сброса всех кешей в приложении
private clearAllCaches = () => {
// Сброс кешей статей
articlesStore.articleList = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
articlesStore.articlePreview = {};
articlesStore.articleData = null;
articlesStore.articleMedia = null;
// Сброс кешей городов
cityStore.cities = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
cityStore.ruCities = { data: [], loaded: false };
cityStore.city = {};
// Сброс кешей стран
countryStore.countries = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
// Сброс кешей перевозчиков
carrierStore.carriers = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
// Сброс кешей станций
stationsStore.stationLists = {
ru: { data: [], loaded: false },
en: { data: [], loaded: false },
zh: { data: [], loaded: false },
};
stationsStore.stationPreview = {};
// Сброс кешей достопримечательностей
sightsStore.sights = [];
sightsStore.sight = null;
// Сброс кешей маршрутов
routeStore.routes = { data: [], loaded: false };
// Сброс кешей транспорта
vehicleStore.vehicles = { data: [], loaded: false };
// Сброс кешей пользователей
userStore.users = { data: [], loaded: false };
// Сброс кешей медиа
mediaStore.media = [];
mediaStore.oneMedia = null;
// Сброс кешей создания и редактирования достопримечательностей
createSightStore.sight = JSON.parse(
JSON.stringify({
city_id: 0,
city: "",
latitude: 0,
longitude: 0,
thumbnail: null,
watermark_lu: null,
watermark_rd: null,
left_article: 0,
preview_media: null,
video_preview: null,
ru: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
en: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
zh: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
})
);
createSightStore.uploadMediaOpen = false;
createSightStore.fileToUpload = null;
createSightStore.needLeaveAgree = false;
editSightStore.sight = {
common: {
id: 0,
city_id: 0,
city: "",
latitude: 0,
longitude: 0,
thumbnail: null,
watermark_lu: null,
watermark_rd: null,
left_article: 0,
preview_media: null,
video_preview: null,
},
ru: {
id: 0,
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
en: {
id: 0,
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
zh: {
id: 0,
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
};
editSightStore.hasLoadedCommon = false;
editSightStore.uploadMediaOpen = false;
editSightStore.fileToUpload = null;
editSightStore.needLeaveAgree = false;
// Сброс кешей устройств
devicesStore.devices = [];
devicesStore.uuid = null;
devicesStore.sendSnapshotModalOpen = false;
// Сброс кешей авторизации (кроме токена)
authStore.payload = null;
authStore.error = null;
authStore.isLoading = false;
// Сброс кешей карты (если они загружены)
try {
// Сбрасываем кеши mapStore если он доступен
if (typeof window !== "undefined" && (window as any).mapStore) {
(window as any).mapStore.routes = [];
(window as any).mapStore.stations = [];
(window as any).mapStore.sights = [];
}
// Сбрасываем кеши MapService если он доступен
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
(window as any).mapServiceInstance.clearCaches();
}
} catch (error) {
console.warn("Не удалось сбросить кеши карты:", error);
}
// Сброс localStorage кешей (кроме токена авторизации)
const token = localStorage.getItem("token");
const rememberedEmail = localStorage.getItem("rememberedEmail");
const rememberedPassword = localStorage.getItem("rememberedPassword");
localStorage.clear();
sessionStorage.clear();
// Восстанавливаем важные данные
if (token) localStorage.setItem("token", token);
if (rememberedEmail)
localStorage.setItem("rememberedEmail", rememberedEmail);
if (rememberedPassword)
localStorage.setItem("rememberedPassword", rememberedPassword);
// Сброс кешей карты (если они есть)
const mapPositionKey = "mapPosition";
const activeSectionKey = "mapActiveSection";
if (localStorage.getItem(mapPositionKey)) {
localStorage.removeItem(mapPositionKey);
}
if (localStorage.getItem(activeSectionKey)) {
localStorage.removeItem(activeSectionKey);
}
// Попытка очистить кеш браузера (если поддерживается)
if ("caches" in window) {
try {
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
return caches.delete(cacheName);
})
);
})
.then(() => {
console.log("Кеш браузера очищен");
})
.catch((error) => {
console.warn("Не удалось очистить кеш браузера:", error);
});
} catch (error) {
console.warn("Кеш браузера не поддерживается:", error);
}
}
// Попытка очистить IndexedDB (если поддерживается)
if ("indexedDB" in window) {
try {
indexedDB
.databases()
.then((databases) => {
return Promise.all(
databases.map((db) => {
if (db.name) {
return indexedDB.deleteDatabase(db.name);
}
return Promise.resolve();
})
);
})
.then(() => {
console.log("IndexedDB очищен");
})
.catch((error) => {
console.warn("Не удалось очистить IndexedDB:", error);
});
} catch (error) {
console.warn("IndexedDB не поддерживается:", error);
}
}
console.log("Все кеши приложения сброшены");
};
getSnapshots = async () => { getSnapshots = async () => {
const response = await authInstance.get(`/snapshots`); const response = await authInstance.get(`/snapshots`);
@ -42,6 +302,10 @@ class SnapshotStore {
}; };
restoreSnapshot = async (id: string) => { restoreSnapshot = async (id: string) => {
// Сначала сбрасываем все кеши
this.clearAllCaches();
// Затем восстанавливаем снапшот
await authInstance.post(`/snapshots/${id}/restore`); await authInstance.post(`/snapshots/${id}/restore`);
}; };

View File

@ -202,7 +202,6 @@ export const DevicesTable = observer(() => {
try { try {
// Create an array of promises for all snapshot requests // Create an array of promises for all snapshot requests
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => { const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
return send(deviceUuid); return send(deviceUuid);
}); });

View File

@ -51,7 +51,7 @@ export const LanguageSwitcher = observer(() => {
key={lang} key={lang}
onClick={() => handleLanguageChange(lang)} onClick={() => handleLanguageChange(lang)}
variant={"contained"} // Highlight the active language variant={"contained"} // Highlight the active language
color={language === lang ? "primary" : "secondary"} color={language === lang ? "primary" : "inherit"}
sx={{ minWidth: "60px" }} // Give buttons a consistent width sx={{ minWidth: "60px" }} // Give buttons a consistent width
> >
{getLanguageLabel(lang)} {getLanguageLabel(lang)}

View File

@ -109,6 +109,7 @@ export const MediaArea = observer(
media_type: m.media_type, media_type: m.media_type,
filename: m.filename, filename: m.filename,
}} }}
height="40px"
/> />
<button <button
className="absolute top-2 right-2" className="absolute top-2 right-2"

View File

@ -12,11 +12,15 @@ export interface MediaData {
export function MediaViewer({ export function MediaViewer({
media, media,
className, className,
height,
width,
fullWidth, fullWidth,
fullHeight, fullHeight,
}: Readonly<{ }: Readonly<{
media?: MediaData; media?: MediaData;
className?: string; className?: string;
height?: string;
width?: string;
fullWidth?: boolean; fullWidth?: boolean;
fullHeight?: boolean; fullHeight?: boolean;
}>) { }>) {
@ -42,8 +46,8 @@ export function MediaViewer({
}/download?token=${token}`} }/download?token=${token}`}
alt={media?.filename} alt={media?.filename}
style={{ style={{
height: fullHeight ? "100%" : "auto", height: fullHeight ? "100%" : height ? height : "auto",
width: fullWidth ? "100%" : "auto", width: fullWidth ? "100%" : width ? width : "auto",
...(media?.filename?.toLowerCase().endsWith(".webp") && { ...(media?.filename?.toLowerCase().endsWith(".webp") && {
maxWidth: "300px", maxWidth: "300px",
maxHeight: "300px", maxHeight: "300px",
@ -59,8 +63,8 @@ export function MediaViewer({
media?.id media?.id
}/download?token=${token}`} }/download?token=${token}`}
style={{ style={{
width: "100%", width: width ? width : "100%",
height: "100%", height: height ? height : "100%",
objectFit: "cover", objectFit: "cover",
borderRadius: 8, borderRadius: 8,
}} }}
@ -76,8 +80,8 @@ export function MediaViewer({
}/download?token=${token}`} }/download?token=${token}`}
alt={media?.filename} alt={media?.filename}
style={{ style={{
height: fullHeight ? "100%" : "auto", height: fullHeight ? "100%" : height ? height : "auto",
width: fullWidth ? "100%" : "auto", width: fullWidth ? "100%" : width ? width : "auto",
}} }}
/> />
)} )}
@ -98,8 +102,8 @@ export function MediaViewer({
src={`${import.meta.env.VITE_KRBL_MEDIA}${ src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id media?.id
}/download?token=${token}`} }/download?token=${token}`}
width={"100%"} width={width ? width : "500px"}
height={"100%"} height={height ? height : "300px"}
/> />
)} )}
@ -108,8 +112,8 @@ export function MediaViewer({
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${ fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id media?.id
}/download?token=${token}`} }/download?token=${token}`}
height="500px" height={height ? height : "500px"}
width="500px" width={width ? width : "500px"}
/> />
)} )}
</Box> </Box>

View File

@ -1,3 +1,4 @@
import React from "react";
import { Stage, useGLTF } from "@react-three/drei"; import { Stage, useGLTF } from "@react-three/drei";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei"; import { OrbitControls } from "@react-three/drei";
@ -5,12 +6,21 @@ import { OrbitControls } from "@react-three/drei";
export const ModelViewer3D = ({ export const ModelViewer3D = ({
fileUrl, fileUrl,
height = "100%", height = "100%",
onLoad,
}: { }: {
fileUrl: string; fileUrl: string;
height: string; height: string;
onLoad?: () => void;
}) => { }) => {
const { scene } = useGLTF(fileUrl); const { scene } = useGLTF(fileUrl);
// Вызываем onLoad когда модель загружена
React.useEffect(() => {
if (onLoad) {
onLoad();
}
}, [scene, onLoad]);
return ( return (
<Canvas style={{ width: "100%", height: height }}> <Canvas style={{ width: "100%", height: height }}>
<ambientLight /> <ambientLight />

View File

@ -1,13 +1,15 @@
import { Button } from "@mui/material"; import { Button, CircularProgress } from "@mui/material";
export const SnapshotRestore = ({ export const SnapshotRestore = ({
onDelete, onDelete,
onCancel, onCancel,
open, open,
loading = false,
}: { }: {
onDelete: () => void; onDelete: () => void;
onCancel: () => void; onCancel: () => void;
open: boolean; open: boolean;
loading?: boolean;
}) => { }) => {
return ( return (
<div <div
@ -23,10 +25,22 @@ export const SnapshotRestore = ({
Это действие нельзя будет отменить. Это действие нельзя будет отменить.
</p> </p>
<div className="flex gap-4 justify-center"> <div className="flex gap-4 justify-center">
<Button variant="contained" color="primary" onClick={onDelete}> <Button
Да variant="contained"
color="primary"
onClick={onDelete}
disabled={loading}
startIcon={
loading ? (
<CircularProgress size={16} color="inherit" />
) : undefined
}
>
{loading ? "Восстановление..." : "Да"}
</Button>
<Button onClick={onCancel} disabled={loading}>
Нет
</Button> </Button>
<Button onClick={onCancel}>Нет</Button>
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because one or more lines are too long