Disabled signup; Map test

This commit is contained in:
cracklesparkle
2024-09-26 12:02:01 +09:00
parent 33f41aaab0
commit 108dc5082c
20 changed files with 833 additions and 445 deletions

View File

@ -31,6 +31,7 @@ export const pages = [
component: <SignIn />, component: <SignIn />,
drawer: false, drawer: false,
dashboard: false, dashboard: false,
enabled: true,
}, },
{ {
label: "", label: "",
@ -39,6 +40,7 @@ export const pages = [
component: <SignUp />, component: <SignUp />,
drawer: false, drawer: false,
dashboard: false, dashboard: false,
enabled: false,
}, },
{ {
label: "", label: "",
@ -47,6 +49,7 @@ export const pages = [
component: <PasswordReset />, component: <PasswordReset />,
drawer: false, drawer: false,
dashboard: false, dashboard: false,
enabled: true,
}, },
{ {
label: "Настройки", label: "Настройки",
@ -55,6 +58,7 @@ export const pages = [
component: <Settings />, component: <Settings />,
drawer: false, drawer: false,
dashboard: true, dashboard: true,
enabled: true,
}, },
{ {
label: "Главная", label: "Главная",
@ -62,7 +66,8 @@ export const pages = [
icon: <Home />, icon: <Home />,
component: <Main />, component: <Main />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "Пользователи", label: "Пользователи",
@ -70,7 +75,8 @@ export const pages = [
icon: <People />, icon: <People />,
component: <Users />, component: <Users />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "Роли", label: "Роли",
@ -78,7 +84,8 @@ export const pages = [
icon: <Shield />, icon: <Shield />,
component: <Roles />, component: <Roles />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "Документы", label: "Документы",
@ -86,7 +93,8 @@ export const pages = [
icon: <Storage />, icon: <Storage />,
component: <Documents />, component: <Documents />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "Отчеты", label: "Отчеты",
@ -94,7 +102,8 @@ export const pages = [
icon: <Assignment />, icon: <Assignment />,
component: <Reports />, component: <Reports />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "Серверы", label: "Серверы",
@ -102,7 +111,8 @@ export const pages = [
icon: <Cloud />, icon: <Cloud />,
component: <Servers />, component: <Servers />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "Котельные", label: "Котельные",
@ -110,7 +120,8 @@ export const pages = [
icon: <Factory />, icon: <Factory />,
component: <Boilers />, component: <Boilers />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: true,
}, },
{ {
label: "API Test", label: "API Test",
@ -118,15 +129,17 @@ export const pages = [
icon: <Api />, icon: <Api />,
component: <ApiTest />, component: <ApiTest />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: false,
}, },
{ {
label: "Карта", label: "ИКС",
path: "/map-test", path: "/map-test",
icon: <Map />, icon: <Map />,
component: <MapTest />, component: <MapTest />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: false,
}, },
{ {
label: "Chunk test", label: "Chunk test",
@ -134,7 +147,8 @@ export const pages = [
icon: <Warning />, icon: <Warning />,
component: <ChunkedUpload />, component: <ChunkedUpload />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: false,
}, },
{ {
label: "Монитор", label: "Монитор",
@ -142,7 +156,8 @@ export const pages = [
icon: <MonitorHeart />, icon: <MonitorHeart />,
component: <MonitorPage />, component: <MonitorPage />,
drawer: true, drawer: true,
dashboard: true dashboard: true,
enabled: false,
}, },
] ]
@ -174,13 +189,13 @@ function App() {
<Router> <Router>
<Routes> <Routes>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
{pages.filter((page) => !page.dashboard).map((page, index) => ( {pages.filter((page) => !page.dashboard).filter((page) => page.enabled).map((page, index) => (
<Route key={`ml-${index}`} path={page.path} element={page.component} /> <Route key={`ml-${index}`} path={page.path} element={page.component} />
))} ))}
</Route> </Route>
<Route element={auth.isAuthenticated ? <DashboardLayout /> : <Navigate to={"/auth/signin"} />}> <Route element={auth.isAuthenticated ? <DashboardLayout /> : <Navigate to={"/auth/signin"} />}>
{pages.filter((page) => page.dashboard).map((page, index) => ( {pages.filter((page) => page.dashboard).filter((page) => page.enabled).map((page, index) => (
<Route key={`dl-${index}`} path={page.path} element={page.component} /> <Route key={`dl-${index}`} path={page.path} element={page.component} />
))} ))}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />

View File

@ -184,7 +184,8 @@ export default function FolderViewer() {
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '16px' gap: '16px',
p: '16px'
}}> }}>
<FileViewer <FileViewer
open={fileViewerModal} open={fileViewerModal}

View File

@ -4,18 +4,18 @@ import 'ol/ol.css'
import Map from 'ol/Map' import Map from 'ol/Map'
import View from 'ol/View' import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction' import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, TileDebug, Vector as VectorSource, XYZ } from 'ol/source' import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer' import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box, Typography } from '@mui/material' import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box, Typography, Drawer, Button, Modal, Accordion, AccordionSummary, AccordionDetails, SxProps, Theme } from '@mui/material'
import { Add, Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Upload, Warning } from '@mui/icons-material' import { Add, Adjust, Api, CircleOutlined, ExpandMore, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Upload, Warning } from '@mui/icons-material'
import { Type } from 'ol/geom/Geometry' import Geometry, { Type } from 'ol/geom/Geometry'
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature' import Feature from 'ol/Feature'
import { SatelliteMapsProvider } from '../../interfaces/map' import { SatelliteMapsProvider } from '../../interfaces/map'
import { containsExtent, Extent, getCenter, getHeight, getWidth } from 'ol/extent' import { containsExtent, Extent, getCenter, getHeight, getWidth } from 'ol/extent'
import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles' import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources' import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import { mapCenter, mapExtent } from './MapConstants' import { mapCenter } from './MapConstants'
import ImageLayer from 'ol/layer/Image' import ImageLayer from 'ol/layer/Image'
import VectorImageLayer from 'ol/layer/VectorImage' import VectorImageLayer from 'ol/layer/VectorImage'
import { LineString, MultiPoint, Point, Polygon, SimpleGeometry } from 'ol/geom' import { LineString, MultiPoint, Point, Polygon, SimpleGeometry } from 'ol/geom'
@ -25,17 +25,28 @@ import { Coordinate } from 'ol/coordinate'
import { Stroke, Fill, Circle as CircleStyle, Style } from 'ol/style' import { Stroke, Fill, Circle as CircleStyle, Style } from 'ol/style'
import { calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils' import { calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils'
import MapBrowserEvent from 'ol/MapBrowserEvent' import MapBrowserEvent from 'ol/MapBrowserEvent'
import { get } from 'ol/proj' import { fromLonLat, get } from 'ol/proj'
import axios from 'axios' import { useCities } from '../../hooks/swrHooks'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
const MapComponent = () => { const MapComponent = () => {
const { cities } = useCities(100, 1)
useEffect(() => {
if (cities) {
cities.map((city: any) => {
citiesLayer.current?.getSource()?.addFeature(new Feature(new Point(fromLonLat([city.longitude, city.width]))))
})
}
}, [cities])
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null) const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
const [currentZ, setCurrentZ] = useState<number | undefined>(undefined) const [currentZ, setCurrentZ] = useState<number | undefined>(undefined)
const [currentX, setCurrentX] = useState<number | undefined>(undefined) const [currentX, setCurrentX] = useState<number | undefined>(undefined)
const [currentY, setCurrentY] = useState<number | undefined>(undefined) const [currentY, setCurrentY] = useState<number | undefined>(undefined)
const [testExtent, setTestExtent] = useState<Extent | null>(null)
const [file, setFile] = useState(null) const [file, setFile] = useState(null)
const [polygonExtent, setPolygonExtent] = useState<Extent | undefined>(undefined) const [polygonExtent, setPolygonExtent] = useState<Extent | undefined>(undefined)
const [bottomLeft, setBottomLeft] = useState<Coordinate | undefined>(undefined) const [bottomLeft, setBottomLeft] = useState<Coordinate | undefined>(undefined)
@ -72,12 +83,19 @@ const MapComponent = () => {
}, },
})) }))
const nodeLayer = useRef<VectorLayer | null>(null)
const nodeLayerSource = useRef<VectorSource>(new VectorSource())
const overlayLayer = useRef<VectorLayer | null>(null) const overlayLayer = useRef<VectorLayer | null>(null)
const overlayLayerSource = useRef<VectorSource>(new VectorSource()) const overlayLayerSource = useRef<VectorSource>(new VectorSource())
const drawingLayer = useRef<VectorLayer | null>(null) const drawingLayer = useRef<VectorLayer | null>(null)
const drawingLayerSource = useRef<VectorSource>(new VectorSource()) const drawingLayerSource = useRef<VectorSource>(new VectorSource())
const citiesLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource()
}))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({ const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
source: regionsLayerSource, source: regionsLayerSource,
style: regionsLayerStyle style: regionsLayerStyle
@ -98,6 +116,21 @@ const MapComponent = () => {
type: currentTool, type: currentTool,
condition: noModifierKeys condition: noModifierKeys
}) })
draw.current.on('drawend', function (s) {
console.log(s.feature.getGeometry()?.getType())
let type = 'POLYGON'
switch (s.feature.getGeometry()?.getType()) {
case 'LineString':
type = 'LINE'
case 'Polygon':
type = 'POLYGON'
}
const coordinates = (s.feature.getGeometry() as SimpleGeometry).getCoordinates()
uploadCoordinates(coordinates, type)
})
map?.current?.addInteraction(draw.current) map?.current?.addInteraction(draw.current)
snap.current = new Snap({ source: drawingLayerSource.current }) snap.current = new Snap({ source: drawingLayerSource.current })
map?.current?.addInteraction(snap.current) map?.current?.addInteraction(snap.current)
@ -595,14 +628,18 @@ const MapComponent = () => {
}, },
}) })
nodeLayer.current = new VectorLayer({
source: nodeLayerSource.current,
style: drawingLayerStyle
})
map.current = new Map({ map.current = new Map({
layers: [baseLayer.current, new TileLayer({ controls: [],
source: new TileDebug(), layers: [baseLayer.current, satLayer.current, regionsLayer.current, citiesLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current, nodeLayer.current],
}), satLayer.current, regionsLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current],
target: mapElement.current as HTMLDivElement, target: mapElement.current as HTMLDivElement,
view: new View({ view: new View({
center: mapCenter, center: mapCenter,//center: fromLonLat([130.401113, 67.797368]),
zoom: 2, zoom: 16,
maxZoom: 21, maxZoom: 21,
//extent: mapExtent, //extent: mapExtent,
}), }),
@ -665,6 +702,27 @@ const MapComponent = () => {
} }
}, [currentTool]) }, [currentTool])
const uploadCoordinates = async (coordinates: any, type: any) => {
try {
const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/nodes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ coordinates, object_id: 1, type: type }) // Replace with actual object_id
});
if (response.ok) {
const data = await response.json();
console.log('Node created:', data);
} else {
console.error('Failed to upload coordinates');
}
} catch (error) {
console.error('Error:', error);
}
};
const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0) const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)
const [statusText, setStatusText] = useState('') const [statusText, setStatusText] = useState('')
@ -712,52 +770,48 @@ const MapComponent = () => {
} }
} }
const mapControlsStyle: SxProps<Theme> = {
borderRadius: '4px',
position: 'absolute',
zIndex: '1',
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? '#FFFFFFAA'
: '#000000AA',
backdropFilter: 'blur(8px)'
}
const { data: nodes } = useSWR('/nodes/all', () => fetcher('/nodes/all', BASE_URL.ems), { revalidateOnFocus: false })
useEffect(() => {
// Draw features based on database data
if (Array.isArray(nodes)) {
nodes.map(node => {
if (node.shape_type === 'LINE') {
let coordinates: Coordinate[] = []
if (Array.isArray(node.shape)) {
node.shape.map((point: any) => {
const coordinate = [point.x as number, point.y as number] as Coordinate
coordinates.push(coordinate)
})
}
//console.log(coordinates)
nodeLayerSource.current.addFeature(new Feature({ geometry: new LineString(coordinates) }))
}
})
}
}, [nodes])
return ( return (
<Stack flex={1} flexDirection='column'> <Box height={'calc(100% - 64px)'} maxHeight={'100%'} flex={'1'} flexGrow={'1'} position={'relative'}>
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}> <Stack
<IconButton title='Добавить подложку'> direction={'column'}
<Add /> sx={{
</IconButton> ...mapControlsStyle,
top: '8px',
<Stack> right: '8px',
<Typography> }}
x: {currentCoordinate?.[0]} divider={<Divider orientation='horizontal' flexItem />}>
</Typography>
<Typography>
y: {currentCoordinate?.[1]}
</Typography>
</Stack>
<Typography>
Z={currentZ}
X={currentX}
Y={currentY}
</Typography>
<IconButton onClick={() => submitOverlay()}>
<Upload />
</IconButton>
</Stack>
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
<Stack flex={1} alignItems='center' justifyContent='center'>
<Slider aria-label="Opacity" min={0} max={1} step={0.001} defaultValue={satelliteOpacity} value={satelliteOpacity} onChange={(_, value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
</Stack>
<MUISelect
variant='standard'
labelId="demo-simple-select-label"
id="demo-simple-select"
value={satMapsProvider}
label="Satellite Provider"
onChange={(e) => setSatMapsProvider(e.target.value as SatelliteMapsProvider)}
>
<MenuItem value={'google'}>Google</MenuItem>
<MenuItem value={'yandex'}>Яндекс</MenuItem>
<MenuItem value={'custom'}>Custom</MenuItem>
</MUISelect>
<IconButton onClick={() => { <IconButton onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res)) fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}> }}>
@ -812,14 +866,113 @@ const MapComponent = () => {
</IconButton> </IconButton>
</Stack> </Stack>
<Box> <Stack
<div id="map-container" ref={mapElement} style={{ width: '100%', height: '600px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div> direction={'column'}
</Box> sx={{
...mapControlsStyle,
maxWidth: '300px',
width: '100%',
top: '8px',
left: '8px',
}} divider={<Divider orientation='horizontal' flexItem />}
>
<Stack direction={'row'}>
<IconButton onClick={() => submitOverlay()}>
<Upload />
</IconButton>
<IconButton title='Добавить подложку'>
<Add />
</IconButton>
</Stack>
<Stack direction={'row'} padding={'8px'} spacing={4}>
<Slider size='small' aria-label="Opacity" min={0} max={1} step={0.001} defaultValue={satelliteOpacity} value={satelliteOpacity} onChange={(_, value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<MUISelect
variant='standard'
labelId="demo-simple-select-label"
id="demo-simple-select"
value={satMapsProvider}
label="Satellite Provider"
onChange={(e) => setSatMapsProvider(e.target.value as SatelliteMapsProvider)}
>
<MenuItem value={'google'}>Google</MenuItem>
<MenuItem value={'yandex'}>Яндекс</MenuItem>
<MenuItem value={'custom'}>Custom</MenuItem>
</MUISelect>
</Stack>
<Accordion disableGutters sx={{ backgroundColor: 'transparent' }} defaultExpanded>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel1-content"
id="panel1-header"
>
<Typography>Объекты</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Typography>
</AccordionDetails>
</Accordion>
</Stack>
<Stack direction={'row'}
sx={{
...mapControlsStyle,
bottom: '8px',
left: '8px',
}}
divider={<Divider orientation='vertical' flexItem />}
>
<Stack>
<Typography>
x: {currentCoordinate?.[0]}
</Typography>
<Typography>
y: {currentCoordinate?.[1]}
</Typography>
</Stack>
<Typography>
Z={currentZ}
X={currentX}
Y={currentY}
</Typography>
</Stack>
<Stack direction={'row'}
sx={{
...mapControlsStyle,
bottom: '8px',
right: '8px',
}}
divider={<Divider orientation='vertical' flexItem />}>
<Stack> <Stack>
{statusText} {statusText}
</Stack> </Stack>
</Stack> </Stack>
<div
id="map-container"
ref={mapElement}
style={{
width: '100%',
height: '100%',
maxHeight: '100%',
position: 'fixed',
flexGrow: 1
}}
>
</div>
</Box>
); );
}; };

View File

@ -5,7 +5,8 @@ import Style from "ol/style/Style";
const drawingLayerStyle: FlatStyleLike = { const drawingLayerStyle: FlatStyleLike = {
'fill-color': 'rgba(255, 255, 255, 0.2)', 'fill-color': 'rgba(255, 255, 255, 0.2)',
'stroke-color': '#ffcc33', //'stroke-color': '#ffcc33',
'stroke-color': '#000000',
'stroke-width': 2, 'stroke-width': 2,
'circle-radius': 7, 'circle-radius': 7,
'circle-fill-color': '#ffcc33', 'circle-fill-color': '#ffcc33',

View File

@ -7,5 +7,6 @@ export const BASE_URL = {
auth: import.meta.env.VITE_API_AUTH_URL, auth: import.meta.env.VITE_API_AUTH_URL,
info: import.meta.env.VITE_API_INFO_URL, info: import.meta.env.VITE_API_INFO_URL,
fuel: import.meta.env.VITE_API_FUEL_URL, fuel: import.meta.env.VITE_API_FUEL_URL,
servers: import.meta.env.VITE_API_SERVERS_URL servers: import.meta.env.VITE_API_SERVERS_URL,
ems: import.meta.env.VITE_API_EMS_URL,
} }

View File

@ -166,7 +166,7 @@ export default function DashboardLayout() {
<Divider /> <Divider />
<List component="nav"> <List component="nav">
{pages.filter((page) => page.drawer).map((item, index) => ( {pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item, index) => (
<ListItem <ListItem
key={index} key={index}
disablePadding disablePadding
@ -204,12 +204,7 @@ export default function DashboardLayout() {
}} }}
> >
<Toolbar /> <Toolbar />
<Container
maxWidth={false}
sx={{ mt: 4, mb: 4 }}
>
<Outlet /> <Outlet />
</Container>
</Box> </Box>
</Box> </Box>
</ThemeProvider> </ThemeProvider>

View File

@ -16,6 +16,13 @@ const mainTheme = createTheme(
].join(',') ].join(',')
}, },
components: { components: {
MuiAppBar: {
// styleOverrides: {
// colorPrimary: {
// backgroundColor: 'gray'
// }
// }
},
MuiListItemButton: { MuiListItemButton: {
defaultProps: { defaultProps: {
//disableRipple: true //disableRipple: true
@ -38,6 +45,7 @@ const mainTheme = createTheme(
}, },
MuiIconButton: { MuiIconButton: {
defaultProps: { defaultProps: {
} }
}, },
MuiIcon: { MuiIcon: {

View File

@ -2,8 +2,6 @@ import FolderViewer from '../components/FolderViewer'
export default function Documents() { export default function Documents() {
return ( return (
<div>
<FolderViewer /> <FolderViewer />
</div>
) )
} }

View File

@ -2,7 +2,7 @@ import { Box, Card, Typography } from "@mui/material";
export default function Main() { export default function Main() {
return ( return (
<Box> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Typography variant='h6' fontWeight='700'> <Typography variant='h6' fontWeight='700'>
Последние файлы Последние файлы
</Typography> </Typography>

View File

@ -2,9 +2,7 @@ import MapComponent from '../components/map/MapComponent'
function MapTest() { function MapTest() {
return ( return (
<div>
<MapComponent /> <MapComponent />
</div>
) )
} }

View File

@ -41,8 +41,7 @@ export default function Reports() {
} }
return ( return (
<> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Box sx={{ display: 'flex', gap: '16px' }}> <Box sx={{ display: 'flex', gap: '16px' }}>
<Autocomplete <Autocomplete
fullWidth fullWidth
@ -121,6 +120,5 @@ export default function Reports() {
}} }}
/> />
</Box> </Box>
</>
) )
} }

View File

@ -31,7 +31,8 @@ export default function Roles() {
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: '16px', gap: '16px',
flexGrow: 1 flexGrow: 1,
p: '16px'
}}> }}>
<Button onClick={() => setOpen(true)}> <Button onClick={() => setOpen(true)}>
Добавить роль Добавить роль

View File

@ -36,7 +36,6 @@ export default function Servers() {
} }
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={currentTab} onChange={(_, value) => <Tabs value={currentTab} onChange={(_, value) =>
@ -65,13 +64,12 @@ export default function Servers() {
<ServerStorage /> <ServerStorage />
</CustomTabPanel> </CustomTabPanel>
<BarChart {/* <BarChart
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]} xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]} series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]}
width={500} width={500}
height={300} height={300}
/> /> */}
</Box>
</Box> </Box>
) )
} }

View File

@ -50,6 +50,7 @@ export default function Users() {
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
gap: "16px", gap: "16px",
p: '16px'
}} }}
> >
<Button onClick={() => setOpen(true)}> <Button onClick={() => setOpen(true)}>

View File

@ -87,13 +87,11 @@ const SignIn = () => {
{isSubmitting ? <CircularProgress size={16} /> : 'Вход'} {isSubmitting ? <CircularProgress size={16} /> : 'Вход'}
</Button> </Button>
<Button fullWidth href="/auth/signup" type="button" variant="text" color="primary"> {/* <Button fullWidth href="/auth/signup" type="button" variant="text" color="primary">
Регистрация Регистрация
</Button> </Button> */}
</Box> </Box>
</Stack> </Stack>
</form> </form>
</Box> </Box>
</Container> </Container>

224
ems/package-lock.json generated
View File

@ -9,16 +9,17 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^5.18.0", "@prisma/client": "^5.19.1",
"axios": "^1.7.4", "axios": "^1.7.4",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.2.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"md5": "^2.3.0", "md5": "^2.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"prisma": "^5.18.0", "pg": "^8.13.0",
"pump": "^3.0.0", "pump": "^3.0.0",
"sharp": "^0.33.5" "sharp": "^0.33.5"
}, },
@ -32,6 +33,7 @@
"@types/pump": "^1.1.3", "@types/pump": "^1.1.3",
"@types/redis": "^4.0.11", "@types/redis": "^4.0.11",
"nodemon": "^3.1.4", "nodemon": "^3.1.4",
"prisma": "^5.19.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.5.4" "typescript": "^5.5.4"
} }
@ -430,9 +432,9 @@
} }
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "5.18.0", "version": "5.19.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz",
"integrity": "sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==", "integrity": "sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==",
"hasInstallScript": true, "hasInstallScript": true,
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
@ -447,43 +449,48 @@
} }
}, },
"node_modules/@prisma/debug": { "node_modules/@prisma/debug": {
"version": "5.18.0", "version": "5.19.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz",
"integrity": "sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==" "integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==",
"devOptional": true
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "5.18.0", "version": "5.19.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz",
"integrity": "sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==", "integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==",
"devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@prisma/debug": "5.18.0", "@prisma/debug": "5.19.1",
"@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
"@prisma/fetch-engine": "5.18.0", "@prisma/fetch-engine": "5.19.1",
"@prisma/get-platform": "5.18.0" "@prisma/get-platform": "5.19.1"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", "version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz",
"integrity": "sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==" "integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==",
"devOptional": true
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "5.18.0", "version": "5.19.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz",
"integrity": "sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==", "integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==",
"devOptional": true,
"dependencies": { "dependencies": {
"@prisma/debug": "5.18.0", "@prisma/debug": "5.19.1",
"@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
"@prisma/get-platform": "5.18.0" "@prisma/get-platform": "5.19.1"
} }
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
"version": "5.18.0", "version": "5.19.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz",
"integrity": "sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==", "integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==",
"devOptional": true,
"dependencies": { "dependencies": {
"@prisma/debug": "5.18.0" "@prisma/debug": "5.19.1"
} }
}, },
"node_modules/@redis/bloom": { "node_modules/@redis/bloom": {
@ -1244,6 +1251,18 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/express-validator": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz",
"integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==",
"dependencies": {
"lodash": "^4.17.21",
"validator": "~13.12.0"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -1592,6 +1611,11 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.defaults": { "node_modules/lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@ -1841,6 +1865,87 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
}, },
"node_modules/pg": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz",
"integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.0",
"pg-protocol": "^1.7.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -1853,19 +1958,58 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prisma": { "node_modules/prisma": {
"version": "5.18.0", "version": "5.19.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.18.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz",
"integrity": "sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==", "integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==",
"devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@prisma/engines": "5.18.0" "@prisma/engines": "5.19.1"
}, },
"bin": { "bin": {
"prisma": "build/index.js" "prisma": "build/index.js"
}, },
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
},
"optionalDependencies": {
"fsevents": "2.3.3"
} }
}, },
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
@ -2181,6 +2325,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/standard-as-callback": { "node_modules/standard-as-callback": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
@ -2374,6 +2526,14 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true "dev": true
}, },
"node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -13,16 +13,17 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"@prisma/client": "^5.18.0", "@prisma/client": "^5.19.1",
"axios": "^1.7.4", "axios": "^1.7.4",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.2.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"md5": "^2.3.0", "md5": "^2.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"prisma": "^5.18.0", "pg": "^8.13.0",
"pump": "^3.0.0", "pump": "^3.0.0",
"sharp": "^0.33.5" "sharp": "^0.33.5"
}, },
@ -36,6 +37,7 @@
"@types/pump": "^1.1.3", "@types/pump": "^1.1.3",
"@types/redis": "^4.0.11", "@types/redis": "^4.0.11",
"nodemon": "^3.1.4", "nodemon": "^3.1.4",
"prisma": "^5.19.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.5.4" "typescript": "^5.5.4"
} }

View File

@ -13,28 +13,17 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model Post { enum ShapeType {
id Int @id @default(autoincrement()) CIRCLE
createdAt DateTime @default(now()) ELLIPSIS
updatedAt DateTime @updatedAt POLYGON
title String @db.VarChar(255) LINE
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
} }
model Profile { model nodes {
id Int @id @default(autoincrement()) id String @id @default(uuid())
bio String? object_id Int?
user User @relation(fields: [userId], references: [id]) shape_type ShapeType
userId Int @unique shape Json @db.Json
} label String @db.Text
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
} }

View File

@ -1,25 +1,27 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express'
import fs from 'fs'; import { PrismaClient } from '@prisma/client'
import path from 'path'; import fs from 'fs'
import axios from 'axios'; import path from 'path'
import axios from 'axios'
import multer from 'multer' import multer from 'multer'
import sharp from 'sharp'; import bodyParser from 'body-parser'
import bodyParser from 'body-parser';
import cors from 'cors' import cors from 'cors'
import { Coordinate, Extent } from './interfaces/map'; import { Coordinate } from './interfaces/map'
import { epsg3857extent } from './constants'; import { generateTilesForZoomLevel } from './utils/tiles'
import { query, validationResult } from 'express-validator'
const app = express(); const prisma = new PrismaClient()
const PORT = process.env.EMS_PORT || 5000; const app = express()
const PORT = process.env.EMS_PORT || 5000
const tileFolder = path.join(__dirname, '..', 'public', 'tile_data'); const tileFolder = path.join(__dirname, '..', 'public', 'tile_data')
const uploadDir = path.join(__dirname, '..', 'public', 'temp'); const uploadDir = path.join(__dirname, '..', 'public', 'temp')
app.use(cors()) app.use(cors())
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }))
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: function (req, file, cb) { destination: function (req, file, cb) {
@ -32,167 +34,69 @@ const storage = multer.diskStorage({
const upload = multer({ storage: storage }) const upload = multer({ storage: storage })
function getTilesPerSide(zoom: number) { app.get('/nodes/all', async (req: Request, res: Response) => {
return Math.pow(2, zoom)
}
function normalize(value: number, min: number, max: number) {
return (value - min) / (max - min)
}
function getTileIndex(normalized: number, tilesPerSide: number) {
return Math.floor(normalized * tilesPerSide)
}
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
const tilesPerSide = getTilesPerSide(zoom)
const minX = extent[0]
const minY = extent[1]
const maxX = extent[2]
const maxY = extent[3]
const xNormalized = normalize(x, minX, maxX)
const yNormalized = normalize(y, minY, maxY)
const tileX = getTileIndex(xNormalized, tilesPerSide)
const tileY = getTileIndex(1 - yNormalized, tilesPerSide)
return { tileX, tileY }
}
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
const deltaX = bottomRight.x - bottomLeft.x
const deltaY = bottomRight.y - bottomLeft.y
const angle = -Math.atan2(deltaY, deltaX)
return angle
}
function roundUpToNearest(number: number, mod: number) {
return Math.floor(number / mod) * mod
}
async function generateTilesForZoomLevel(file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) {
const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI
const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel)
const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft.x, topLeft.y, epsg3857extent, zoomLevel)
const { tileX: trX, tileY: trY } = getGridCellPosition(topRight.x, topRight.y, epsg3857extent, zoomLevel)
const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight.x, topRight.y, epsg3857extent, zoomLevel)
const minX = Math.min(blX, tlX, trX, brX)
const maxX = Math.max(blX, tlX, trX, brX)
const minY = Math.min(blY, tlY, trY, brY)
const maxY = Math.max(blY, tlY, trY, brY)
const mapWidth = Math.abs(epsg3857extent[0] - epsg3857extent[2])
const mapHeight = Math.abs(epsg3857extent[1] - epsg3857extent[3])
const tilesH = Math.sqrt(Math.pow(4, zoomLevel))
const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel)))
const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel)))
let minPosX = minX - (tilesH / 2)
let maxPosX = maxX - (tilesH / 2) + 1
let minPosY = -(minY - (tilesH / 2))
let maxPosY = -(maxY - (tilesH / 2) + 1)
const newMinX = tileWidth * minPosX
const newMaxX = tileWidth * maxPosX
const newMinY = tileHeight * maxPosY
const newMaxY = tileHeight * minPosY
const paddingLeft = Math.abs(polygonExtent[0] - newMinX)
const paddingRight = Math.abs(polygonExtent[2] - newMaxX)
const paddingTop = Math.abs(polygonExtent[3] - newMaxY)
const paddingBottom = Math.abs(polygonExtent[1] - newMinY)
const pixelWidth = Math.abs(minX - (maxX + 1)) * 256
const pixelHeight = Math.abs(minY - (maxY + 1)) * 256
const width = Math.abs(newMinX - newMaxX)
try { try {
let perPixel = width / pixelWidth const nodes = await prisma.nodes.findMany()
// constraint to original image width res.json(nodes)
const imageMetadata = await sharp(file.path).metadata().then(res => { } catch (error) {
if (res.width) { console.error('Error getting node:', error);
perPixel = pixelWidth <= res.width ? perPixel : width / res.width res.status(500).json({ error: 'Failed to get node' });
} }
}) })
const paddingLeftPixel = paddingLeft / perPixel
const paddingRightPixel = paddingRight / perPixel
const paddingTopPixel = paddingTop / perPixel
const paddingBottomPixel = paddingBottom / perPixel
const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / perPixel
const boundsHeightPixel = Math.abs(polygonExtent[1] - polygonExtent[3]) / perPixel
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) {
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true });
}
const initialZoomImage = await sharp(path.join(uploadDir, file.filename))
.rotate(Math.ceil(angleDegrees), {
background: '#00000000'
})
.resize({
width: Math.ceil(boundsWidthPixel),
height: Math.ceil(boundsHeightPixel),
background: '#00000000'
})
.extend({
top: Math.ceil(paddingTopPixel),
left: Math.ceil(paddingLeftPixel),
bottom: Math.ceil(paddingBottomPixel),
right: Math.ceil(paddingRightPixel),
background: '#00000000'
})
.toFormat('png')
.toBuffer({ resolveWithObject: true })
if (initialZoomImage) {
await sharp(initialZoomImage.data.buffer)
.resize({
width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))),
height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))),
})
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
.then(async (res) => {
let left = 0
for (let x = minX; x <= maxX; x++) {
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) {
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true });
}
let top = 0
for (let y = minY; y <= maxY; y++) {
console.log(`z: ${zoomLevel} x: ${x} y: ${y}`)
app.get('/nodes', query('id').isString().isUUID(), async (req: Request, res: Response) => {
try { try {
await sharp(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png')) const result = validationResult(req)
.extract({ if (!result.isEmpty()) {
width: res.width / Math.abs(minX - (maxX + 1)), return res.send({ errors: result.array() })
height: res.height / Math.abs(minY - (maxY + 1)), }
left: left,
top: top const { id } = req.params
})
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png')) const node = await prisma.nodes.findFirst({
.then(() => { where: {
top = top + res.height / Math.abs(minY - (maxY + 1)) id: id
}
}) })
res.json(node)
} catch (error) { } catch (error) {
console.log(error) console.error('Error getting node:', error);
res.status(500).json({ error: 'Failed to get node' });
} }
})
app.post('/nodes', async (req: Request, res: Response) => {
try {
const { coordinates, object_id, type } = req.body;
// Convert the incoming array of coordinates into the shape structure
const shape = coordinates.map((point: number[]) => ({
object_id: object_id || null,
x: point[0],
y: point[1]
}));
console.log(shape)
// Create a new node in the database
const node = await prisma.nodes.create({
data: {
object_id: object_id || null, // Nullable if object_id is not provided
shape_type: type, // You can adjust this dynamically
shape: shape, // Store the shape array as Json[]
label: 'Default'
} }
left = left + res.width / Math.abs(minX - (maxX + 1)) });
}
}) res.status(201).json(node);
}
} catch (error) { } catch (error) {
console.log(error) console.error('Error creating node:', error);
res.status(500).json({ error: 'Failed to create node' });
} }
} })
app.post('/upload', upload.single('file'), async (req: Request, res: Response) => { app.post('/upload', upload.single('file'), async (req: Request, res: Response) => {
const { extentMinX, extentMinY, extentMaxX, extentMaxY, blX, blY, tlX, tlY, trX, trY, brX, brY } = req.body const { extentMinX, extentMinY, extentMaxX, extentMaxY, blX, blY, tlX, tlY, trX, trY, brX, brY } = req.body
@ -204,7 +108,7 @@ app.post('/upload', upload.single('file'), async (req: Request, res: Response) =
if (req.file) { if (req.file) {
for (let z = 0; z <= 21; z++) { for (let z = 0; z <= 21; z++) {
await generateTilesForZoomLevel(req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z) await generateTilesForZoomLevel(uploadDir, tileFolder, req.file, [extentMinX, extentMinY, extentMaxX, extentMaxY], bottomLeft, topLeft, topRight, bottomRight, z)
} }
} }
@ -214,37 +118,37 @@ app.post('/upload', upload.single('file'), async (req: Request, res: Response) =
const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => { const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise<Buffer> => {
const url = provider === 'google' const url = provider === 'google'
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}` ? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
: `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`; : `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`
const response = await axios.get(url, { responseType: 'arraybuffer' }); const response = await axios.get(url, { responseType: 'arraybuffer' })
return response.data; return response.data
} }
app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => { app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => {
const { provider, z, x, y } = req.params; const { provider, z, x, y } = req.params
if (!['google', 'yandex', 'custom'].includes(provider)) { if (!['google', 'yandex', 'custom'].includes(provider)) {
return res.status(400).send('Invalid provider'); return res.status(400).send('Invalid provider')
} }
const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`); const tilePath = provider === 'custom' ? path.join(tileFolder, provider, z, x, `${y}.png`) : path.join(tileFolder, provider, z, x, `${y}.jpg`)
if (fs.existsSync(tilePath)) { if (fs.existsSync(tilePath)) {
return res.sendFile(tilePath); return res.sendFile(tilePath)
} else { } else {
if (provider !== 'custom') { if (provider !== 'custom') {
try { try {
const tileData = await fetchTileFromAPI(provider, z, x, y); const tileData = await fetchTileFromAPI(provider, z, x, y)
fs.mkdirSync(path.dirname(tilePath), { recursive: true }); fs.mkdirSync(path.dirname(tilePath), { recursive: true })
fs.writeFileSync(tilePath, tileData); fs.writeFileSync(tilePath, tileData)
res.contentType('image/jpeg'); res.contentType('image/jpeg')
res.send(tileData); res.send(tileData)
} catch (error) { } catch (error) {
console.error('Error fetching tile from API:', error); console.error('Error fetching tile from API:', error)
res.status(500).send('Error fetching tile from API'); res.status(500).send('Error fetching tile from API')
} }
} else { } else {
res.status(404).send('Tile is not generated or not provided') res.status(404).send('Tile is not generated or not provided')

167
ems/src/utils/tiles.ts Normal file
View File

@ -0,0 +1,167 @@
import sharp from "sharp"
import { epsg3857extent } from "../constants"
import { Coordinate, Extent } from "../interfaces/map"
import path from "path"
import fs from 'fs'
function getTilesPerSide(zoom: number) {
return Math.pow(2, zoom)
}
function normalize(value: number, min: number, max: number) {
return (value - min) / (max - min)
}
function getTileIndex(normalized: number, tilesPerSide: number) {
return Math.floor(normalized * tilesPerSide)
}
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
const tilesPerSide = getTilesPerSide(zoom)
const minX = extent[0]
const minY = extent[1]
const maxX = extent[2]
const maxY = extent[3]
const xNormalized = normalize(x, minX, maxX)
const yNormalized = normalize(y, minY, maxY)
const tileX = getTileIndex(xNormalized, tilesPerSide)
const tileY = getTileIndex(1 - yNormalized, tilesPerSide)
return { tileX, tileY }
}
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
const deltaX = bottomRight.x - bottomLeft.x
const deltaY = bottomRight.y - bottomLeft.y
const angle = -Math.atan2(deltaY, deltaX)
return angle
}
function roundUpToNearest(number: number, mod: number) {
return Math.floor(number / mod) * mod
}
export async function generateTilesForZoomLevel(uploadDir: string, tileFolder: string, file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) {
const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI
const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel)
const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft.x, topLeft.y, epsg3857extent, zoomLevel)
const { tileX: trX, tileY: trY } = getGridCellPosition(topRight.x, topRight.y, epsg3857extent, zoomLevel)
const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight.x, topRight.y, epsg3857extent, zoomLevel)
const minX = Math.min(blX, tlX, trX, brX)
const maxX = Math.max(blX, tlX, trX, brX)
const minY = Math.min(blY, tlY, trY, brY)
const maxY = Math.max(blY, tlY, trY, brY)
const mapWidth = Math.abs(epsg3857extent[0] - epsg3857extent[2])
const mapHeight = Math.abs(epsg3857extent[1] - epsg3857extent[3])
const tilesH = Math.sqrt(Math.pow(4, zoomLevel))
const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel)))
const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel)))
let minPosX = minX - (tilesH / 2)
let maxPosX = maxX - (tilesH / 2) + 1
let minPosY = -(minY - (tilesH / 2))
let maxPosY = -(maxY - (tilesH / 2) + 1)
const newMinX = tileWidth * minPosX
const newMaxX = tileWidth * maxPosX
const newMinY = tileHeight * maxPosY
const newMaxY = tileHeight * minPosY
const paddingLeft = Math.abs(polygonExtent[0] - newMinX)
const paddingRight = Math.abs(polygonExtent[2] - newMaxX)
const paddingTop = Math.abs(polygonExtent[3] - newMaxY)
const paddingBottom = Math.abs(polygonExtent[1] - newMinY)
const pixelWidth = Math.abs(minX - (maxX + 1)) * 256
const pixelHeight = Math.abs(minY - (maxY + 1)) * 256
const width = Math.abs(newMinX - newMaxX)
try {
let perPixel = width / pixelWidth
// constraint to original image width
const imageMetadata = await sharp(file.path).metadata().then(res => {
if (res.width) {
perPixel = pixelWidth <= res.width ? perPixel : width / res.width
}
})
const paddingLeftPixel = paddingLeft / perPixel
const paddingRightPixel = paddingRight / perPixel
const paddingTopPixel = paddingTop / perPixel
const paddingBottomPixel = paddingBottom / perPixel
const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / perPixel
const boundsHeightPixel = Math.abs(polygonExtent[1] - polygonExtent[3]) / perPixel
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) {
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true });
}
const initialZoomImage = await sharp(path.join(uploadDir, file.filename))
.rotate(Math.ceil(angleDegrees), {
background: '#00000000'
})
.resize({
width: Math.ceil(boundsWidthPixel),
height: Math.ceil(boundsHeightPixel),
background: '#00000000'
})
.extend({
top: Math.ceil(paddingTopPixel),
left: Math.ceil(paddingLeftPixel),
bottom: Math.ceil(paddingBottomPixel),
right: Math.ceil(paddingRightPixel),
background: '#00000000'
})
.toFormat('png')
.toBuffer({ resolveWithObject: true })
if (initialZoomImage) {
await sharp(initialZoomImage.data.buffer)
.resize({
width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))),
height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))),
})
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
.then(async (res) => {
let left = 0
for (let x = minX; x <= maxX; x++) {
if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) {
fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true });
}
let top = 0
for (let y = minY; y <= maxY; y++) {
console.log(`z: ${zoomLevel} x: ${x} y: ${y}`)
try {
await sharp(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
.extract({
width: res.width / Math.abs(minX - (maxX + 1)),
height: res.height / Math.abs(minY - (maxY + 1)),
left: left,
top: top
})
.toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png'))
.then(() => {
top = top + res.height / Math.abs(minY - (maxY + 1))
})
} catch (error) {
console.log(error)
}
}
left = left + res.width / Math.abs(minX - (maxX + 1))
}
})
}
} catch (error) {
console.log(error)
}
}