Add Map page with basic logic

This commit is contained in:
2025-06-02 18:02:02 +03:00
parent a8777a974a
commit 5814e65953
15 changed files with 2576 additions and 132 deletions
package.json
src
app
router
pages
CreateMediaPage
EditMediaPage
MapPage
MediaListPage
PreviewMediaPage
index.ts
shared
config
store
MediaStore
VehicleStore
widgets
DevicesTable
MediaViewer
tsconfig.tsbuildinfoyarn.lock

@ -22,6 +22,7 @@
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"mobx": "^6.13.7", "mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0", "mobx-react-lite": "^4.1.0",
"ol": "^10.5.0",
"path": "^0.12.7", "path": "^0.12.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

@ -5,6 +5,11 @@ import {
LoginPage, LoginPage,
MainPage, MainPage,
SightPage, SightPage,
MapPage,
MediaListPage,
PreviewMediaPage,
EditMediaPage,
// CreateMediaPage,
} from "@pages"; } from "@pages";
import { authStore, createSightStore, editSightStore } from "@shared"; import { authStore, createSightStore, editSightStore } from "@shared";
import { Layout } from "@widgets"; import { Layout } from "@widgets";
@ -71,6 +76,11 @@ export const Router = () => {
<Route path="sight/:id" element={<EditSightPage />} /> <Route path="sight/:id" element={<EditSightPage />} />
<Route path="sight/create" element={<CreateSightPage />} /> <Route path="sight/create" element={<CreateSightPage />} />
<Route path="devices" element={<DevicesPage />} /> <Route path="devices" element={<DevicesPage />} />
<Route path="map" element={<MapPage />} />
<Route path="media" element={<MediaListPage />} />
<Route path="media/:id" element={<PreviewMediaPage />} />
<Route path="media/:id/edit" element={<EditMediaPage />} />
{/* <Route path="media/create" element={<CreateMediaPage />} /> */}
</Route> </Route>
</Routes> </Routes>
); );

@ -0,0 +1,126 @@
// import { Button, Paper, Typography, Box, Alert, Snackbar } from "@mui/material";
// import { useNavigate } from "react-router-dom";
// import { ArrowLeft, Upload } from "lucide-react";
// import { observer } from "mobx-react-lite";
// import { useState, DragEvent, useRef, useEffect } from "react";
// import { editSightStore, UploadMediaDialog } from "@shared";
// export const CreateMediaPage = observer(() => {
// const navigate = useNavigate();
// const [isDragging, setIsDragging] = useState(false);
// const [error, setError] = useState<string | null>(null);
// const [success, setSuccess] = useState(false);
// const fileInputRef = useRef<HTMLInputElement>(null);
// const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
// e.preventDefault();
// e.stopPropagation();
// setIsDragging(false);
// const files = Array.from(e.dataTransfer.files);
// if (files.length > 0) {
// editSightStore.fileToUpload = files[0];
// setUploadDialogOpen(true);
// }
// };
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
// e.preventDefault();
// setIsDragging(true);
// };
// const handleDragLeave = () => {
// setIsDragging(false);
// };
// const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
// const files = e.target.files;
// if (files && files.length > 0) {
// editSightStore.fileToUpload = files[0];
// setUploadDialogOpen(true);
// }
// };
// const handleUploadSuccess = () => {
// setSuccess(true);
// setUploadDialogOpen(false);
// };
// return (
// <div className="w-full h-full p-8">
// <div className="flex items-center gap-4 mb-8">
// <Button
// variant="outlined"
// startIcon={<ArrowLeft size={20} />}
// onClick={() => navigate("/media")}
// >
// Назад
// </Button>
// <Typography variant="h5">Загрузка медиафайла</Typography>
// </div>
// <Paper
// elevation={3}
// className={`w-full h-[60vh] flex flex-col items-center justify-center p-8 transition-colors ${
// isDragging ? "bg-blue-50 border-2 border-blue-500" : "bg-gray-50"
// }`}
// onDrop={handleDrop}
// onDragOver={handleDragOver}
// onDragLeave={handleDragLeave}
// >
// <input
// type="file"
// ref={fileInputRef}
// className="hidden"
// onChange={handleFileSelect}
// accept="image/*,video/*,.glb,.gltf"
// />
// <Box className="flex flex-col items-center gap-4 text-center">
// <Upload size={48} className="text-gray-400" />
// <Typography variant="h6" className="text-gray-600">
// Перетащите файл сюда или
// </Typography>
// <Button
// variant="contained"
// onClick={() => fileInputRef.current?.click()}
// startIcon={<Upload size={20} />}
// >
// Выберите файл
// </Button>
// <Typography variant="body2" className="text-gray-500 mt-4">
// Поддерживаемые форматы: JPG, PNG, GIF, MP4, WebM, GLB, GLTF
// </Typography>
// </Box>
// </Paper>
// <UploadMediaDialog
// open={uploadDialogOpen}
// onClose={() => setUploadDialogOpen(false)}
// afterUpload={handleUploadSuccess}
// />
// <Snackbar
// open={success}
// autoHideDuration={2000}
// onClose={() => setSuccess(false)}
// >
// <Alert severity="success" onClose={() => setSuccess(false)}>
// Медиафайл успешно загружен
// </Alert>
// </Snackbar>
// <Snackbar
// open={!!error}
// autoHideDuration={6000}
// onClose={() => setError(null)}
// >
// <Alert severity="error" onClose={() => setError(null)}>
// {error}
// </Alert>
// </Snackbar>
// </div>
// );
// });

@ -0,0 +1,255 @@
import { useEffect, useState, useRef } from "react";
import { observer } from "mobx-react-lite";
import { useNavigate, useParams } from "react-router-dom";
import {
Box,
Button,
CircularProgress,
FormControl,
InputLabel,
MenuItem,
Paper,
Select,
TextField,
Typography,
Alert,
Snackbar,
} from "@mui/material";
import { Save, ArrowLeft } from "lucide-react";
import { authInstance, mediaStore, MEDIA_TYPE_LABELS } from "@shared";
import { MediaViewer } from "@widgets";
export const EditMediaPage = observer(() => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [newFile, setNewFile] = useState<File | null>(null);
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
const [mediaFilename, setMediaFilename] = useState(media?.filename ?? "");
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
useEffect(() => {
if (id) {
mediaStore.getOneMedia(id);
}
console.log(newFile);
console.log(uploadDialogOpen);
}, [id]);
useEffect(() => {
if (media) {
setMediaName(media.media_name);
setMediaFilename(media.filename);
setMediaType(media.media_type);
}
}, [media]);
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
// e.preventDefault();
// e.stopPropagation();
// setIsDragging(false);
// const files = Array.from(e.dataTransfer.files);
// if (files.length > 0) {
// setNewFile(files[0]);
// setMediaFilename(files[0].name);
// setUploadDialogOpen(true); // Open dialog on file drop
// }
// };
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
// e.preventDefault();
// setIsDragging(true);
// };
// const handleDragLeave = () => {
// setIsDragging(false);
// };
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
setNewFile(files[0]);
setMediaFilename(files[0].name);
setUploadDialogOpen(true); // Open dialog on file selection
}
};
const handleSave = async () => {
if (!id) return;
setIsLoading(true);
setError(null);
try {
await authInstance.patch(`/media/${id}`, {
media_name: mediaName,
filename: mediaFilename,
type: mediaType,
});
// If a new file was selected, the actual file upload will happen
// via the UploadMediaDialog. We just need to make sure the metadata
// is updated correctly before or after.
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
setSuccess(true);
handleUploadSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save media");
} finally {
setIsLoading(false);
}
};
const handleUploadSuccess = () => {
// After successful upload in the dialog, refresh media data if needed
if (id) {
mediaStore.getOneMedia(id);
}
setNewFile(null); // Clear the new file state after successful upload
setUploadDialogOpen(false);
setSuccess(true);
};
if (!media && id) {
// Only show loading if an ID is present and media is not yet loaded
return (
<Box className="flex justify-center items-center h-screen">
<CircularProgress />
</Box>
);
}
return (
<Box className="p-6 max-w-4xl mx-auto">
<Box className="flex items-center gap-4 mb-6">
<Button
variant="outlined"
startIcon={<ArrowLeft size={20} />}
onClick={() => navigate("/media")}
>
Назад
</Button>
<Typography variant="h5">Редактирование медиа</Typography>
</Box>
<Paper className="p-6">
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileSelect}
accept="image/*,video/*,.glb,.gltf"
/>
<Box className="flex flex-col gap-6">
<Box className="flex gap-4">
<TextField
fullWidth
value={mediaName}
onChange={(e) => setMediaName(e.target.value)}
label="Название медиа"
disabled={isLoading}
/>
<TextField
fullWidth
value={mediaFilename}
onChange={(e) => setMediaFilename(e.target.value)}
label="Название файла"
disabled={isLoading}
/>
</Box>
<FormControl fullWidth sx={{ width: "50%" }}>
<InputLabel>Тип медиа</InputLabel>
<Select
value={mediaType}
label="Тип медиа"
onChange={(e) => setMediaType(Number(e.target.value))}
disabled={isLoading}
>
{Object.entries(MEDIA_TYPE_LABELS).map(([type, label]) => (
<MenuItem key={type} value={Number(type)}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
<Box className="flex gap-6">
<Paper
elevation={2}
sx={{
flex: 1,
p: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: 400,
}}
>
<MediaViewer
media={{
id: id || "",
media_type: mediaType,
filename: mediaFilename,
}}
/>
</Paper>
<Box className="flex flex-col gap-4 self-end">
<Button
variant="contained"
color="primary"
startIcon={<Save size={20} />}
onClick={handleSave}
disabled={isLoading || (!mediaName && !mediaFilename)}
>
{isLoading ? <CircularProgress size={20} /> : "Сохранить"}
</Button>
{/* Only show "Replace file" button if no new file is currently selected */}
</Box>
</Box>
{error && (
<Typography color="error" className="mt-2">
{error}
</Typography>
)}
{success && (
<Typography color="success.main" className="mt-2">
Медиа успешно сохранено
</Typography>
)}
</Box>
</Paper>
<Snackbar
open={!!error}
autoHideDuration={6000}
onClose={() => setError(null)}
>
<Alert severity="error" onClose={() => setError(null)}>
{error}
</Alert>
</Snackbar>
<Snackbar
open={success}
autoHideDuration={2000}
onClose={() => setSuccess(false)}
>
<Alert severity="success" onClose={() => setSuccess(false)}>
Медиа успешно сохранено
</Alert>
</Snackbar>
</Box>
);
});

1594
src/pages/MapPage/index.tsx Normal file

File diff suppressed because it is too large Load Diff

@ -0,0 +1,76 @@
import { Button, TableBody } from "@mui/material";
import { TableRow, TableCell } from "@mui/material";
import { Table, TableHead } from "@mui/material";
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
const rows = (media: any[]) => {
return media.map((row) => ({
id: row.id,
media_name: row.media_name,
media_type:
MEDIA_TYPE_LABELS[row.media_type as keyof typeof MEDIA_TYPE_LABELS],
}));
};
export const MediaListPage = observer(() => {
const { media, getMedia, deleteMedia } = mediaStore;
const navigate = useNavigate();
useEffect(() => {
getMedia();
}, []);
const currentRows = rows(media);
return (
<>
<div className="flex justify-end p-3 gap-5">
<Button
variant="contained"
color="primary"
onClick={() => navigate("/sight/create")}
>
Создать
</Button>
</div>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">Название</TableCell>
<TableCell align="center">Тип</TableCell>
<TableCell align="center">Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{currentRows.map((row) => (
<TableRow
key={row.media_name}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell align="center">{row.media_name}</TableCell>
<TableCell align="center">{row.media_type}</TableCell>
<TableCell align="center">
<div className="flex gap-7 justify-center">
<button onClick={() => navigate(`/media/${row.id}`)}>
<Eye size={20} className="text-green-500" />
</button>
<button onClick={() => navigate(`/media/${row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => deleteMedia(row.id)}>
<Trash2 size={20} className="text-red-500" />
</button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
});

@ -0,0 +1,44 @@
import { mediaStore } from "@shared";
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { MediaViewer } from "../../widgets/MediaViewer/index";
import { observer } from "mobx-react-lite";
import { Download } from "lucide-react";
import { Button } from "@mui/material";
export const PreviewMediaPage = observer(() => {
const { id } = useParams();
const { oneMedia, getOneMedia } = mediaStore;
useEffect(() => {
getOneMedia(id!);
}, []);
return (
<div className="w-full h-[80vh] flex flex-col justify-center items-center gap-4">
<div className="w-full h-full flex justify-center items-center">
<MediaViewer className="w-full h-full" media={oneMedia!} />
</div>
{oneMedia && (
<div className="flex flex-col items-center gap-4 bg-[#998879] p-4 rounded-md">
<p className="text-white text-center">
Чтобы скачать файл, нажмите на кнопку ниже
</p>
<Button
variant="contained"
color="primary"
startIcon={<Download size={16} />}
component="a"
href={`${
import.meta.env.VITE_KRBL_MEDIA
}${id}/download?token=${localStorage.getItem("token")}`}
target="_blank"
>
Скачать
</Button>
</div>
)}
</div>
);
});

@ -4,3 +4,8 @@ export * from "./LoginPage";
export * from "./DevicesPage"; export * from "./DevicesPage";
export * from "./SightPage"; export * from "./SightPage";
export * from "./CreateSightPage"; export * from "./CreateSightPage";
export * from "./MapPage";
export * from "./MediaListPage";
export * from "./PreviewMediaPage";
export * from "./EditMediaPage";
// export * from "./CreateMediaPage";

@ -1,5 +1,13 @@
import { authStore } from "@shared"; import { authStore } from "@shared";
import { Power, LucideIcon, Building2, MonitorSmartphone } from "lucide-react"; import {
Power,
LucideIcon,
Building2,
MonitorSmartphone,
Map,
BookImage,
Newspaper,
} from "lucide-react";
export const DRAWER_WIDTH = 300; export const DRAWER_WIDTH = 300;
interface NavigationItem { interface NavigationItem {
@ -21,12 +29,30 @@ export const NAVIGATION_ITEMS: {
icon: Building2, icon: Building2,
path: "/sight", path: "/sight",
}, },
{
id: "map",
label: "Карта",
icon: Map,
path: "/map",
},
{ {
id: "devices", id: "devices",
label: "Устройства", label: "Устройства",
icon: MonitorSmartphone, icon: MonitorSmartphone,
path: "/devices", path: "/devices",
}, },
{
id: "media",
label: "Медиа",
icon: BookImage,
path: "/media",
},
{
id: "articles",
label: "Статьи",
icon: Newspaper,
path: "/articles",
},
], ],
secondary: [ secondary: [
{ {

@ -10,7 +10,7 @@ type Media = {
class MediaStore { class MediaStore {
media: Media[] = []; media: Media[] = [];
oneMedia: Media | null = null;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
@ -22,6 +22,60 @@ class MediaStore {
this.media = [...response.data]; this.media = [...response.data];
}); });
}; };
deleteMedia = async (id: string) => {
await authInstance.delete(`/media/${id}`);
this.media = this.media.filter((media) => media.id !== id);
};
getOneMedia = async (id: string) => {
this.oneMedia = null;
const response = await authInstance.get(`/media/${id}`);
runInAction(() => {
this.oneMedia = response.data;
});
};
updateMedia = async (id: string, data: Partial<Media>) => {
const response = await authInstance.patch(`/media/${id}`, data);
runInAction(() => {
// Update in media array
const index = this.media.findIndex((m) => m.id === id);
if (index !== -1) {
this.media[index] = { ...this.media[index], ...response.data };
}
// Update oneMedia if it's the current media being viewed
if (this.oneMedia?.id === id) {
this.oneMedia = { ...this.oneMedia, ...response.data };
}
});
return response.data;
};
updateMediaFile = async (id: string, file: File, filename: string) => {
const formData = new FormData();
formData.append("file", file);
formData.append("filename", filename);
const response = await authInstance.patch(`/media/${id}/file`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
runInAction(() => {
// Update in media array
const index = this.media.findIndex((m) => m.id === id);
if (index !== -1) {
this.media[index] = { ...this.media[index], ...response.data };
}
// Update oneMedia if it's the current media being viewed
if (this.oneMedia?.id === id) {
this.oneMedia = { ...this.oneMedia, ...response.data };
}
});
return response.data;
};
} }
export const mediaStore = new MediaStore(); export const mediaStore = new MediaStore();

@ -1,7 +1,7 @@
import { API_URL, authInstance } from "@shared"; import { API_URL, authInstance } from "@shared";
import { makeAutoObservable } from "mobx"; import { makeAutoObservable } from "mobx";
type Vehicle = { export type Vehicle = {
vehicle: { vehicle: {
id: number; id: number;
tail_number: number; tail_number: number;

@ -11,15 +11,25 @@ import {
devicesStore, devicesStore,
Modal, Modal,
snapshotStore, snapshotStore,
vehicleStore, vehicleStore, // Not directly used in this component's rendering logic anymore
} from "@shared"; } from "@shared"; // Assuming @shared exports these
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Button, Checkbox, Typography } from "@mui/material"; // Import Typography for the modal message import { Button, Checkbox, Typography } from "@mui/material";
import { Vehicle } from "@shared";
import { toast } from "react-toastify";
const formatDate = (dateString: string | undefined) => { export type ConnectedDevice = string;
interface Snapshot {
ID: string; // Assuming ID is string based on usage
Name: string;
// Add other snapshot properties if needed
}
// --- HELPER FUNCTIONS ---
const formatDate = (dateString: string | null) => {
if (!dateString) return "Нет данных"; if (!dateString) return "Нет данных";
try { try {
const date = new Date(dateString); const date = new Date(dateString);
return new Intl.DateTimeFormat("ru-RU", { return new Intl.DateTimeFormat("ru-RU", {
@ -32,244 +42,368 @@ const formatDate = (dateString: string | undefined) => {
hour12: false, hour12: false,
}).format(date); }).format(date);
} catch (error) { } catch (error) {
console.error("Error formatting date:", error);
return "Некорректная дата"; return "Некорректная дата";
} }
}; };
type TableRowData = {
tail_number: number;
online: boolean;
lastUpdate: string | null;
gps: boolean;
media: boolean;
connection: boolean;
device_uuid: string | null;
};
function createData( function createData(
uuid: string, tail_number: number,
online: boolean, online: boolean,
lastUpdate: string, lastUpdate: string | null,
gps: boolean, gps: boolean,
media: boolean, media: boolean,
connection: boolean connection: boolean,
) { device_uuid: string | null
return { uuid, online, lastUpdate, gps, media, connection }; ): TableRowData {
return {
tail_number,
online,
lastUpdate,
gps,
media,
connection,
device_uuid,
};
} }
// Keep the rows function as you provided it, without additional filters // This function transforms the raw device data (which includes vehicle and device_status)
const rows = (vehicles: any[]) => { // into the format expected by the table. It now filters for devices that have a UUID.
const transformDevicesToRows = (
vehicles: Vehicle[]
// devices: ConnectedDevice[]
): TableRowData[] => {
return vehicles.map((vehicle) => { return vehicles.map((vehicle) => {
const uuid = vehicle.vehicle.uuid;
if (!uuid)
return {
tail_number: vehicle.vehicle.tail_number,
online: false,
lastUpdate: null,
gps: false,
media: false,
connection: false,
device_uuid: null,
};
return createData( return createData(
vehicle?.vehicle?.tail_number ?? "1243000", // Using tail_number as UUID, as in your original code vehicle.vehicle.tail_number,
vehicle?.device_status?.online ?? false, vehicle.device_status?.online ?? false,
vehicle?.device_status?.last_update, vehicle.device_status?.last_update ?? null,
vehicle?.device_status?.gps_ok, vehicle.device_status?.gps_ok ?? false,
vehicle?.device_status?.media_service_ok, vehicle.device_status?.media_service_ok ?? false,
vehicle?.device_status?.is_connected vehicle.device_status?.is_connected ?? false,
uuid
); );
}); });
}; };
export const DevicesTable = observer(() => { export const DevicesTable = observer(() => {
const { const {
devices,
getDevices, getDevices,
// uuid, // This 'uuid' from devicesStore refers to a *single* selected device, not for batch actions. setSelectedDevice,
setSelectedDevice, // Useful for individual device actions like 'Reload Status'
sendSnapshotModalOpen, sendSnapshotModalOpen,
toggleSendSnapshotModal, toggleSendSnapshotModal,
} = devicesStore; } = devicesStore;
const { snapshots, getSnapshots } = snapshotStore;
const { getVehicles } = vehicleStore;
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
// Get the current list of rows displayed in the table const { snapshots, getSnapshots } = snapshotStore;
const currentRows = rows(devices); const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
const { devices } = devicesStore;
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
// Transform the raw devices data into rows suitable for the table
// This will also filter out devices without a UUID, as those cannot be acted upon.
const currentTableRows = transformDevicesToRows(
vehicles as Vehicle[]
// devices as ConnectedDevice[]
);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
await getVehicles(); await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices
await getDevices(); await getDevices(); // This should fetch the combined vehicle/device_status data
await getSnapshots(); await getSnapshots();
}; };
fetchData(); fetchData();
}, []); }, [getDevices, getSnapshots]); // Added dependencies
// Determine if all visible devices are selected
const isAllSelected = const isAllSelected =
currentRows.length > 0 && selectedDevices.length === currentRows.length; currentTableRows.length > 0 &&
selectedDeviceUuids.length === currentTableRows.length;
const handleSelectAllDevices = () => { const handleSelectAllDevices = () => {
if (isAllSelected) { if (isAllSelected) {
// If all are currently selected, deselect all setSelectedDeviceUuids([]);
setSelectedDevices([]);
} else { } else {
// Otherwise, select all device UUIDs from the current rows // Select all device UUIDs from the *currently visible and selectable* rows
setSelectedDevices(currentRows.map((row) => row.uuid)); setSelectedDeviceUuids(
currentTableRows.map((row) => row.device_uuid ?? "")
);
} }
}; };
const handleSelectDevice = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSelectDevice = (
const deviceUuid = event.target.value; event: React.ChangeEvent<HTMLInputElement>,
deviceUuid: string
) => {
if (event.target.checked) { if (event.target.checked) {
setSelectedDevices((prevSelected) => [...prevSelected, deviceUuid]); setSelectedDeviceUuids((prevSelected) => [...prevSelected, deviceUuid]);
} else { } else {
setSelectedDevices((prevSelected) => setSelectedDeviceUuids((prevSelected) =>
prevSelected.filter((uuid) => uuid !== deviceUuid) prevSelected.filter((uuid) => uuid !== deviceUuid)
); );
} }
}; };
// This function now opens the modal for selected devices
const handleOpenSendSnapshotModal = () => { const handleOpenSendSnapshotModal = () => {
if (selectedDevices.length > 0) { if (selectedDeviceUuids.length > 0) {
toggleSendSnapshotModal(); toggleSendSnapshotModal();
} }
}; };
const handleReloadStatus = async (uuid: string) => { const handleReloadStatus = async (uuid: string) => {
setSelectedDevice(uuid); // Set the active device in store for context if needed setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere
try {
await authInstance.post(`/devices/${uuid}/request-status`); await authInstance.post(`/devices/${uuid}/request-status`);
await getDevices(); // Refresh devices after status request await getDevices(); // Refresh devices to show updated status
} catch (error) {
console.error(`Error requesting status for device ${uuid}:`, error);
// Optionally: show a user-facing error message
}
}; };
// This function now handles sending snapshots to ALL selected devices
const handleSendSnapshotAction = async (snapshotId: string) => { const handleSendSnapshotAction = async (snapshotId: string) => {
if (selectedDeviceUuids.length === 0) return;
try { try {
for (const deviceUuid of selectedDevices) { // Create an array of promises for all snapshot requests
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`); console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
// Ensure you are using the correct API endpoint for force-snapshot return authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
await authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
snapshot_id: snapshotId, snapshot_id: snapshotId,
}); });
} });
// After all requests are sent
await getDevices(); // Refresh the device list to show updated status // Wait for all promises to settle (either resolve or reject)
setSelectedDevices([]); // Clear the selection await Promise.allSettled(snapshotPromises);
// After all requests are attempted
await getDevices(); // Refresh the device list
setSelectedDeviceUuids([]); // Clear the selection
toggleSendSnapshotModal(); // Close the modal toggleSendSnapshotModal(); // Close the modal
} catch (error) { } catch (error) {
console.error("Error sending snapshots:", error); // This catch block might not be hit if Promise.allSettled is used,
// You might want to show an error notification to the user here // as it doesn't reject on individual promise failures.
// Individual errors should be handled if needed within the .map or by checking results.
console.error("Error in snapshot sending process:", error);
} }
}; };
return ( return (
<> <>
<TableContainer component={Paper}> <TableContainer component={Paper} sx={{ mt: 2 }}>
<div className="flex justify-end p-3 gap-3"> <div className="flex justify-end p-3 gap-2">
{" "}
{/* Changed gap to 3 for slightly less space */}
<Button <Button
variant="contained" variant="outlined" // Changed to outlined for distinction
color="primary"
onClick={handleSelectAllDevices} onClick={handleSelectAllDevices}
size="small"
> >
{isAllSelected ? "Снять выбор со всех" : "Выбрать все"} {isAllSelected ? "Снять выбор" : "Выбрать все"}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
disabled={selectedDevices.length === 0} disabled={selectedDeviceUuids.length === 0}
onClick={handleOpenSendSnapshotModal} // Call the new handler onClick={handleOpenSendSnapshotModal}
size="small"
> >
Отправить снапшот ({selectedDevices.length}) Отправить снапшот ({selectedDeviceUuids.length})
</Button> </Button>
</div> </div>
<Table sx={{ minWidth: 650 }} aria-label="simple table"> <Table sx={{ minWidth: 650 }} aria-label="devices table" size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell align="center" padding="checkbox"> <TableCell padding="checkbox">
{" "}
{/* Added padding="checkbox" */}
<Checkbox <Checkbox
indeterminate={
selectedDeviceUuids.length > 0 &&
selectedDeviceUuids.length < currentTableRows.length
}
checked={isAllSelected} checked={isAllSelected}
onChange={handleSelectAllDevices} onChange={handleSelectAllDevices}
inputProps={{ "aria-label": "select all devices" }} inputProps={{ "aria-label": "select all devices" }}
size="small"
/> />
</TableCell> </TableCell>
<TableCell align="center">Бортовой номер</TableCell> <TableCell align="center">Борт. номер</TableCell>
<TableCell align="center">Онлайн</TableCell> <TableCell align="center">Онлайн</TableCell>
<TableCell align="center">Последнее обновление</TableCell> <TableCell align="center">Обновлено</TableCell>
<TableCell align="center">ГПС</TableCell> <TableCell align="center">GPS</TableCell>
<TableCell align="center">Медиа-данные</TableCell> <TableCell align="center">Медиа</TableCell>
<TableCell align="center">Подключение</TableCell> <TableCell align="center">Связь</TableCell>
<TableCell align="center">Перезапросить</TableCell> <TableCell align="center">Действия</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{currentRows.map((row) => ( {currentTableRows.map((row) => (
<TableRow <TableRow
key={row.uuid} // Use row.uuid as key for consistent rendering key={row.tail_number}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }} hover
role="checkbox"
aria-checked={selectedDeviceUuids.includes(
row.device_uuid ?? ""
)}
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
onClick={(event) => {
// Allow clicking row to toggle checkbox, if not clicking on button
if (
(event.target as HTMLElement).closest("button") === null &&
(event.target as HTMLElement).closest(
'input[type="checkbox"]'
) === null
) {
handleSelectDevice(
{
target: {
checked: !selectedDeviceUuids.includes(
row.device_uuid ?? ""
),
},
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
row.device_uuid ?? ""
);
}
}}
sx={{
cursor: "pointer",
"&:last-child td, &:last-child th": { border: 0 },
}}
> >
<TableCell align="center" padding="checkbox"> <TableCell padding="checkbox">
{" "}
{/* Added padding="checkbox" */}
<Checkbox <Checkbox
className="h-full" checked={selectedDeviceUuids.includes(
onChange={handleSelectDevice} row.device_uuid ?? ""
value={row.uuid} )}
checked={selectedDevices.includes(row.uuid)} // THIS IS THE KEY CHANGE onChange={(event) =>
handleSelectDevice(event, row.device_uuid ?? "")
}
size="small"
/> />
</TableCell> </TableCell>
<TableCell
<TableCell align="center" component="th" scope="row"> align="center"
{row.uuid} component="th"
scope="row"
id={`device-label-${row.device_uuid}`}
>
{row.tail_number}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<div className="flex items-center justify-center">
{row.online ? ( {row.online ? (
<Check className="m-auto text-green-500" /> <Check size={18} className="text-green-600" />
) : ( ) : (
<X className="m-auto text-red-500" /> <X size={18} className="text-red-600" />
)} )}
</div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<div className="flex items-center justify-center">
{formatDate(row.lastUpdate)} {formatDate(row.lastUpdate)}
</div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<div className="flex items-center justify-center">
{row.gps ? ( {row.gps ? (
<Check className="m-auto text-green-500" /> <Check size={18} className="text-green-600" />
) : ( ) : (
<X className="m-auto text-red-500" /> <X size={18} className="text-red-600" />
)} )}
</div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<div className="flex items-center justify-center">
{row.media ? ( {row.media ? (
<Check className="m-auto text-green-500" /> <Check size={18} className="text-green-600" />
) : ( ) : (
<X className="m-auto text-red-500" /> <X size={18} className="text-red-600" />
)} )}
</div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<div className="flex items-center justify-center">
{row.connection ? ( {row.connection ? (
<Check className="m-auto text-green-500" /> <Check size={18} className="text-green-600" />
) : ( ) : (
<X className="m-auto text-red-500" /> <X size={18} className="text-red-600" />
)} )}
</div>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<button onClick={() => handleReloadStatus(row.uuid)}> <Button
<RotateCcw className="m-auto" /> onClick={async (e) => {
</button> e.stopPropagation();
try {
if (
row.device_uuid &&
devices.find((device) => device === row.device_uuid)
) {
await handleReloadStatus(row.device_uuid);
await getDevices();
toast.success("Статус устройства обновлен");
} else {
toast.error("Нет связи с устройством");
}
} catch (error) {
toast.error("Ошибка сервера");
}
}}
title="Перезапросить статус"
size="small"
variant="text"
>
<RotateCcw size={16} />
</Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{currentTableRows.length === 0 && (
<TableRow>
<TableCell colSpan={8} align="center">
Нет устройств для отображения.
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}> <Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
<Typography variant="h6" component="p" sx={{ mb: 2 }}> <Typography variant="h6" component="h2" sx={{ mb: 1 }}>
Выбрать снапшот для{" "} Отправить снапшот
<strong className="text-blue-600">{selectedDevices.length}</strong>{" "}
устройств
</Typography> </Typography>
<div className="mt-5 flex flex-col gap-2 max-h-[300px] overflow-y-auto"> <Typography variant="body1" sx={{ mb: 2 }}>
{snapshots && snapshots.length > 0 ? ( Выбрано устройств:{" "}
snapshots.map((snapshot) => ( <strong className="text-blue-600">
{selectedDeviceUuids.length}
</strong>
</Typography>
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
{snapshots && (snapshots as Snapshot[]).length > 0 ? ( // Cast snapshots
(snapshots as Snapshot[]).map((snapshot) => (
<Button <Button
variant="outlined" variant="outlined"
fullWidth
onClick={() => handleSendSnapshotAction(snapshot.ID)} onClick={() => handleSendSnapshotAction(snapshot.ID)}
sx={{
p: 1.5, // Adjust padding
borderRadius: 2, // Adjust border radius
backgroundColor: "white", // Ensure background is white
"&:hover": {
backgroundColor: "grey.100", // Light hover effect
},
}}
key={snapshot.ID} key={snapshot.ID}
sx={{ justifyContent: "flex-start" }}
> >
{snapshot.Name} {snapshot.Name}
</Button> </Button>
@ -280,6 +414,15 @@ export const DevicesTable = observer(() => {
</Typography> </Typography>
)} )}
</div> </div>
<Button
onClick={toggleSendSnapshotModal}
color="inherit"
variant="outlined"
sx={{ mt: 3 }}
fullWidth
>
Отмена
</Button>
</Modal> </Modal>
</> </>
); );

@ -9,7 +9,10 @@ export interface MediaData {
filename?: string; filename?: string;
} }
export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) { export function MediaViewer({
media,
className,
}: Readonly<{ media?: MediaData; className?: string }>) {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
return ( return (
<Box <Box
@ -22,6 +25,7 @@ export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) {
justifyContent: "center", justifyContent: "center",
margin: "0 auto", margin: "0 auto",
}} }}
className={className}
> >
{media?.media_type === 1 && ( {media?.media_type === 1 && (
<img <img

@ -1 +1 @@
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"} {"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createmediapage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editmediapage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/medialistpage/index.tsx","./src/pages/previewmediapage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}

106
yarn.lock

@ -684,6 +684,11 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@petamoriken/float16@^3.4.7":
version "3.9.2"
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.9.2.tgz#217a5d349f3655b8e286be447e0ed1eae063a78f"
integrity sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==
"@photo-sphere-viewer/core@^5.13.2": "@photo-sphere-viewer/core@^5.13.2":
version "5.13.2" version "5.13.2"
resolved "https://registry.yarnpkg.com/@photo-sphere-viewer/core/-/core-5.13.2.tgz#518f27a2b7ca5a80068d8922183a9999a1b33ad1" resolved "https://registry.yarnpkg.com/@photo-sphere-viewer/core/-/core-5.13.2.tgz#518f27a2b7ca5a80068d8922183a9999a1b33ad1"
@ -1093,6 +1098,11 @@
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz"
integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
"@types/rbush@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-4.0.0.tgz#b327bf54952e9c924ea6702c36904c2ce1d47f35"
integrity sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==
"@types/react-dom@^19.1.2": "@types/react-dom@^19.1.2":
version "19.1.5" version "19.1.5"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz"
@ -1637,6 +1647,11 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0" es-errors "^1.3.0"
gopd "^1.2.0" gopd "^1.2.0"
earcut@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.1.tgz#f60b3f671c5657cca9d3e131c5527c5dde00ef38"
integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==
easymde@^2.20.0: easymde@^2.20.0:
version "2.20.0" version "2.20.0"
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz" resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
@ -1983,6 +1998,20 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
geotiff@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-2.1.3.tgz#993f40f2aa6aa65fb1e0451d86dd22ca8e66910c"
integrity sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==
dependencies:
"@petamoriken/float16" "^3.4.7"
lerc "^3.0.0"
pako "^2.0.4"
parse-headers "^2.0.2"
quick-lru "^6.1.1"
web-worker "^1.2.0"
xml-utils "^1.0.2"
zstddec "^0.1.0"
get-intrinsic@^1.2.6: get-intrinsic@^1.2.6:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
@ -2365,6 +2394,11 @@ keyv@^4.5.4:
dependencies: dependencies:
json-buffer "3.0.1" json-buffer "3.0.1"
lerc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb"
integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==
levn@^0.4.1: levn@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
@ -2923,6 +2957,17 @@ object-assign@^4.1.1:
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
ol@^10.5.0:
version "10.5.0"
resolved "https://registry.yarnpkg.com/ol/-/ol-10.5.0.tgz#5831dc55fe5eb5a09cb4193c9289c1b5a0a0f0ca"
integrity sha512-nHFx8gkGmvYImsa7iKkwUnZidd5gn1XbMZd9GNOorvm9orjW9gQvT3Naw/MjIasVJ3cB9EJUdCGR2EFAulMHsQ==
dependencies:
"@types/rbush" "4.0.0"
earcut "^3.0.0"
geotiff "^2.1.3"
pbf "4.0.1"
rbush "^4.0.0"
optionator@^0.9.3: optionator@^0.9.3:
version "0.9.4" version "0.9.4"
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
@ -2949,6 +2994,11 @@ p-locate@^5.0.0:
dependencies: dependencies:
p-limit "^3.0.2" p-limit "^3.0.2"
pako@^2.0.4:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@ -2969,6 +3019,11 @@ parse-entities@^4.0.0:
is-decimal "^2.0.0" is-decimal "^2.0.0"
is-hexadecimal "^2.0.0" is-hexadecimal "^2.0.0"
parse-headers@^2.0.2:
version "2.0.6"
resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.6.tgz#7940f0abe5fe65df2dd25d4ce8800cb35b49d01c"
integrity sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==
parse-json@^5.0.0: parse-json@^5.0.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz"
@ -3014,6 +3069,13 @@ path@^0.12.7:
process "^0.11.1" process "^0.11.1"
util "^0.10.3" util "^0.10.3"
pbf@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pbf/-/pbf-4.0.1.tgz#ad9015e022b235dcdbe05fc468a9acadf483f0d4"
integrity sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==
dependencies:
resolve-protobuf-schema "^2.1.0"
picocolors@^1.1.1: picocolors@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
@ -3088,6 +3150,11 @@ property-information@^7.0.0:
resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz" resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz"
integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
protocol-buffers-schema@^3.3.1:
version "3.6.0"
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03"
integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==
proxy-from-env@^1.1.0: proxy-from-env@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
@ -3103,6 +3170,23 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-lru@^6.1.1:
version "6.1.2"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-6.1.2.tgz#e9a90524108629be35287d0b864e7ad6ceb3659e"
integrity sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==
quickselect@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603"
integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==
rbush@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/rbush/-/rbush-4.0.1.tgz#1f55afa64a978f71bf9e9a99bc14ff84f3cb0d6d"
integrity sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==
dependencies:
quickselect "^3.0.0"
react-dom@^19.1.0: react-dom@^19.1.0:
version "19.1.0" version "19.1.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz"
@ -3254,6 +3338,13 @@ resolve-from@^4.0.0:
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-protobuf-schema@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==
dependencies:
protocol-buffers-schema "^3.3.1"
resolve@^1.19.0: resolve@^1.19.0:
version "1.22.10" version "1.22.10"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz"
@ -3696,6 +3787,11 @@ web-namespaces@^2.0.0:
resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz" resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
web-worker@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5"
integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==
webgl-constants@^1.1.1: webgl-constants@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855" resolved "https://registry.yarnpkg.com/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855"
@ -3718,6 +3814,11 @@ word-wrap@^1.2.5:
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
xml-utils@^1.0.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.10.2.tgz#436b39ccc25a663ce367ea21abb717afdea5d6b1"
integrity sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==
yallist@^3.0.2: yallist@^3.0.2:
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
@ -3738,6 +3839,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zstddec@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.1.0.tgz#7050f3f0e0c3978562d0c566b3e5a427d2bad7ec"
integrity sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==
zustand@^4.3.2: zustand@^4.3.2:
version "4.5.7" version "4.5.7"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55"