NestJS backend rewrite; migrate client to FluentUI V9
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useBoilers } from '../hooks/swrHooks'
|
||||
import { Stack, Text } from '@mantine/core'
|
||||
import CustomTable from '../components/CustomTable'
|
||||
import { Text } from '@fluentui/react-components'
|
||||
|
||||
function Boilers() {
|
||||
const [boilersPage, setBoilersPage] = useState(1)
|
||||
@@ -25,82 +25,47 @@ function Boilers() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Stack w={'100%'} h={'100%'} p='sm'>
|
||||
<Text size="xl" fw={600}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '1rem',
|
||||
width: '100%',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<Text size={600} weight='bold'>
|
||||
Котельные
|
||||
</Text>
|
||||
|
||||
{boilers &&
|
||||
<CustomTable data={boilers} columns={[
|
||||
{
|
||||
accessorKey: 'id_object',
|
||||
name: 'id_object',
|
||||
header: 'ID',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
accessorKey: 'boiler_name',
|
||||
name: 'boiler_name',
|
||||
header: 'Название',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
accessorKey: 'boiler_code',
|
||||
name: 'boiler_code',
|
||||
header: 'Код',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
accessorKey: 'id_city',
|
||||
name: 'id_city',
|
||||
header: 'Город',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'dictionary'
|
||||
},
|
||||
{
|
||||
accessorKey: 'activity',
|
||||
name: 'activity',
|
||||
header: 'Активен',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'boolean'
|
||||
},
|
||||
]} />
|
||||
}
|
||||
|
||||
{/* {boilers &&
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{boilersColumns.map(column => (
|
||||
<Table.Th key={column.field}>{column.headerName}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{boilers.map((boiler: IBoiler) => (
|
||||
<Table.Tr key={boiler.id_object}>
|
||||
{boilersColumns.map(column => {
|
||||
if (column.field === 'activity') {
|
||||
return (
|
||||
boiler.activity ? (
|
||||
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
|
||||
<Badge fullWidth variant="light">
|
||||
Активен
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
|
||||
) : (
|
||||
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
|
||||
<Badge color="gray" fullWidth variant="light">
|
||||
Отключен
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
)
|
||||
)
|
||||
}
|
||||
else return (
|
||||
<Table.Td key={`${boiler.id_object}-${column.field}`}>{boiler[column.field as keyof IBoiler]}</Table.Td>
|
||||
)
|
||||
})}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
} */}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Flex } from '@mantine/core'
|
||||
import ServerHardware from '../components/ServerHardware'
|
||||
|
||||
const ComponentTest = () => {
|
||||
return (
|
||||
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '1rem', padding: '1rem' }}>
|
||||
<ServerHardware />
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
import { Stack, Tabs } from '@mantine/core'
|
||||
import useSWR from 'swr'
|
||||
import { BASE_URL } from '../constants'
|
||||
import { fetcher } from '../http/axiosInstance'
|
||||
import { useState } from 'react'
|
||||
import CustomTable from '../components/CustomTable'
|
||||
import { Tab, TabList } from '@fluentui/react-components'
|
||||
|
||||
const DBManager = () => {
|
||||
const { data: tablesData } = useSWR(`/db/tables`, (key) => fetcher(key, BASE_URL.ems), {
|
||||
revalidateOnFocus: false
|
||||
})
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<string | unknown>(undefined)
|
||||
|
||||
return (
|
||||
<Stack w={'100%'} h={'100%'} p='xs'>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', padding: '0.5rem' }}>
|
||||
{tablesData && Array.isArray(tablesData) && tablesData.length > 0 &&
|
||||
<Tabs w='100%' h='80%'>
|
||||
<Stack h='100%'>
|
||||
<Tabs.List>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<TabList selectedValue={selectedTab} onTabSelect={(_, data) => setSelectedTab(data.value)}>
|
||||
{tablesData.map(table => (
|
||||
<Tabs.Tab key={table.tablename} value={table.tablename}>
|
||||
<Tab key={table.tablename} value={table.tablename}>
|
||||
{table.tablename}
|
||||
</Tabs.Tab>
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</TabList>
|
||||
|
||||
{tablesData.map(table => (
|
||||
<Tabs.Panel h='100%' key={table.tablename} value={table.tablename} w='100%'>
|
||||
<TableData tablename={table.tablename} />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Stack>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
{tablesData.map((table) => {
|
||||
if (table.tablename === selectedTab)
|
||||
return (
|
||||
<TableData tablename={table.tablename} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
{/* <Card withBorder radius='sm'>
|
||||
<Stack>
|
||||
@@ -42,7 +47,7 @@ const DBManager = () => {
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card> */}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,9 +70,9 @@ const TableData = ({
|
||||
{columnsData && rowsData && Array.isArray(columnsData) && Array.isArray(rowsData) && columnsData.length > 0 &&
|
||||
<CustomTable data={rowsData} columns={columnsData.map(column => (
|
||||
{
|
||||
accessorKey: column.column_name,
|
||||
name: column.column_name,
|
||||
header: column.column_name,
|
||||
cell: (info) => JSON.stringify(info.getValue()).length > 30 ? [JSON.stringify(info.getValue()).substring(0, 30), '...'].join('') : JSON.stringify(info.getValue()),
|
||||
type: 'string',
|
||||
}
|
||||
))} />
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ActionIcon, Button, Flex, Input, Loader, LoadingOverlay, Modal, Overlay, Table, Tabs, TextInput, useMantineColorScheme } from "@mantine/core";
|
||||
import { IconMathMax, IconPlus, IconTableMinus, IconTablePlus } from "@tabler/icons-react";
|
||||
import { FuelExpenseDto, FuelExpenseDtoHeaders, FuelLimitDto, FuelLimitDtoHeaders } from "../dto/fuel/fuel.dto";
|
||||
import { Modal, useMantineColorScheme } from "@mantine/core";
|
||||
import { IconMathMax, IconPlus, IconTableMinus } from "@tabler/icons-react";
|
||||
import { FuelExpenseDtoHeaders, FuelLimitDtoHeaders } from "../dto/fuel/fuel.dto";
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "../http/axiosInstanceNest";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { DateInput, DatePicker } from '@mantine/dates'
|
||||
import { DateInput } from '@mantine/dates'
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { AllCommunityModule, ColDef, ModuleRegistry } from 'ag-grid-community'
|
||||
import { Button, Field, Input, Spinner, Tab, TabList } from "@fluentui/react-components";
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule])
|
||||
|
||||
@@ -92,9 +92,9 @@ export default function FuelPage() {
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(tables[0])
|
||||
|
||||
const { data, isLoading } = useSWR(currentTab.get, () => fetcher(currentTab.get), { revalidateOnFocus: false })
|
||||
const { isLoading } = useSWR(currentTab.get, () => fetcher(currentTab.get), { revalidateOnFocus: false })
|
||||
|
||||
const [openedCreateModal, { open: openCreateModal, close: closeCreateModal }] = useDisclosure(false)
|
||||
const [openCreateModel, setOpenCreateModal] = useState(false)
|
||||
|
||||
const { colorScheme } = useMantineColorScheme()
|
||||
|
||||
@@ -108,49 +108,52 @@ export default function FuelPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalCreate openedCreateModal={openedCreateModal} closeCreateModal={closeCreateModal} currentTab={currentTab} />
|
||||
<ModalCreate openedCreateModal={openCreateModel} closeCreateModal={() => setOpenCreateModal(false)} currentTab={currentTab} />
|
||||
|
||||
<Tabs defaultValue={tables[0].value} w='100%' onChange={(tab) => setCurrentTab(tables.find(table => table.value === tab) || tables[0])}>
|
||||
<Tabs.List>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||
<TabList defaultValue={tables[0].value} selectedValue={currentTab.value}>
|
||||
{tables.map((table, index) => (
|
||||
<Tabs.Tab key={index} value={table.value} leftSection={table.icon}>
|
||||
<Tab key={index} value={table.value} icon={table.icon} onClick={() => setCurrentTab(table)}>
|
||||
{table.label}
|
||||
</Tabs.Tab>
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</TabList>
|
||||
|
||||
<Flex p='sm'>
|
||||
<Button leftSection={<IconPlus />} onClick={openCreateModal}>
|
||||
<div style={{ display: 'flex', padding: '1rem' }}>
|
||||
<Button appearance='primary' icon={<IconPlus />} onClick={() => setOpenCreateModal(true)}>
|
||||
Добавить
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{tables.map((table, index) => (
|
||||
<Tabs.Panel key={index} value={table.value} w='100%' h='100%'>
|
||||
{isLoading ?
|
||||
<Flex w='100%' justify={'center'} p='md'>
|
||||
<Loader />
|
||||
</Flex>
|
||||
:
|
||||
<>
|
||||
<AgGridReact
|
||||
//rowData={data}
|
||||
rowData={[
|
||||
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {}),
|
||||
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
|
||||
]}
|
||||
columnDefs={Object.keys(table.headers).map((header) => ({
|
||||
field: header
|
||||
})) as ColDef[]}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
{tables.map((table, index) => {
|
||||
if (table.value === currentTab.value) {
|
||||
return (
|
||||
isLoading ?
|
||||
<div style={{ display: 'flex', width: '100%', justifyContent: 'center', padding: '1rem' }}>
|
||||
<Spinner />
|
||||
</div>
|
||||
:
|
||||
<>
|
||||
<AgGridReact
|
||||
key={index}
|
||||
//rowData={data}
|
||||
rowData={[
|
||||
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {}),
|
||||
Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
|
||||
]}
|
||||
columnDefs={Object.keys(table.headers).map((header) => ({
|
||||
field: header
|
||||
})) as ColDef[]}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -164,7 +167,9 @@ const ModalCreate = ({
|
||||
closeCreateModal: () => void
|
||||
currentTab: ITableSchema
|
||||
}) => {
|
||||
const { register, handleSubmit, reset, watch, formState: { errors, isSubmitting, dirtyFields, isValid } } = useForm({
|
||||
const { register, handleSubmit,
|
||||
//formState: { errors, isSubmitting, dirtyFields, isValid }
|
||||
} = useForm({
|
||||
mode: 'onChange',
|
||||
})
|
||||
|
||||
@@ -174,9 +179,7 @@ const ModalCreate = ({
|
||||
|
||||
return (
|
||||
<Modal withinPortal opened={openedCreateModal} onClose={closeCreateModal}>
|
||||
<LoadingOverlay visible={isSubmitting} />
|
||||
|
||||
<Flex direction='column' gap='sm' component='form' onSubmit={handleSubmit(onSubmit)}>
|
||||
<form style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} onSubmit={handleSubmit(onSubmit)}>
|
||||
{currentTab.post_include.map((header, index) => {
|
||||
switch (header.field_type) {
|
||||
case 'date':
|
||||
@@ -185,23 +188,27 @@ const ModalCreate = ({
|
||||
)
|
||||
case 'text':
|
||||
return (
|
||||
<TextInput key={index} label={header.field} {...register(header.field, {
|
||||
required: true
|
||||
})} />
|
||||
<Field key={index} label={header.field}>
|
||||
<Input {...register(header.field, {
|
||||
required: true
|
||||
})} />
|
||||
</Field>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<TextInput key={index} label={header.field} {...register(header.field, {
|
||||
required: true
|
||||
})} />
|
||||
<Field key={index} label={header.field}>
|
||||
<Input {...register(header.field, {
|
||||
required: true
|
||||
})} />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
})}
|
||||
|
||||
<Button mt='xl' type='submit'>
|
||||
<Button style={{ marginTop: '2rem' }} appearance="primary" type='submit'>
|
||||
Добавить
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, Flex, SimpleGrid, Text } from "@mantine/core";
|
||||
import { IconBuildingFactory2, IconFiles, IconMap, IconReport, IconServer, IconShield, IconUsers } from "@tabler/icons-react";
|
||||
import { ReactNode } from "react";
|
||||
import { CompoundButton, Text } from "@fluentui/react-components";
|
||||
import { BuildingColor, CloudColor, DocumentColor, FormColor, MapFilled, PeopleListColor, ShieldColor } from "@fluentui/react-icons";
|
||||
//import { IconBuildingFactory2, IconFiles, IconMap, IconReport, IconServer, IconShield, IconUsers } from "@tabler/icons-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Main() {
|
||||
@@ -8,47 +8,56 @@ export default function Main() {
|
||||
|
||||
interface CustomCardProps {
|
||||
link: string;
|
||||
icon: ReactNode;
|
||||
icon: any;
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
}
|
||||
const CustomCard = ({
|
||||
link,
|
||||
icon,
|
||||
label
|
||||
label,
|
||||
secondaryLabel
|
||||
}: CustomCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
<CompoundButton
|
||||
onClick={() => navigate(link)}
|
||||
withBorder
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
icon={icon}
|
||||
secondaryContent={secondaryLabel}
|
||||
>
|
||||
<Flex mih='50'>
|
||||
{icon}
|
||||
</Flex>
|
||||
|
||||
<Text fw={500} size="lg" mt="md">
|
||||
<Text weight={'bold'} size={400}>
|
||||
{label}
|
||||
</Text>
|
||||
</Card>
|
||||
</CompoundButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex w={'100%'} h={'100%'} direction='column' gap='sm' p='sm'>
|
||||
<Text size="xl" fw={700}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '1rem',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<Text size={600} weight='bold'>
|
||||
Главная
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ xs: 1, md: 3 }}>
|
||||
<CustomCard link="/user" icon={<IconUsers size='50' color="#6495ED" />} label="Пользователи" />
|
||||
<CustomCard link="/role" icon={<IconShield size='50' color="#6495ED" />} label="Роли" />
|
||||
<CustomCard link="/documents" icon={<IconFiles size='50' color="#6495ED" />} label="Документы" />
|
||||
<CustomCard link="/reports" icon={<IconReport size='50' color="#6495ED" />} label="Отчеты" />
|
||||
<CustomCard link="/servers" icon={<IconServer size='50' color="#6495ED" />} label="Серверы" />
|
||||
<CustomCard link="/boilers" icon={<IconBuildingFactory2 size='50' color="#6495ED" />} label="Котельные" />
|
||||
<CustomCard link="/map-test" icon={<IconMap size='50' color="#6495ED" />} label="ИКС" />
|
||||
</SimpleGrid>
|
||||
|
||||
</Flex>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<CustomCard link="/user" icon={<PeopleListColor color="#6495ED" />} label="Пользователи" secondaryLabel="Управление пользователями"/>
|
||||
<CustomCard link="/role" icon={<ShieldColor color="#6495ED" />} label="Роли" />
|
||||
<CustomCard link="/documents" icon={<DocumentColor color="#6495ED" />} label="Документы" secondaryLabel="Обзор файлов/документов"/>
|
||||
<CustomCard link="/reports" icon={<FormColor color="#6495ED" />} label="Отчеты" secondaryLabel="Просмотр и создание отчетных документов"/>
|
||||
<CustomCard link="/servers" icon={<CloudColor color="#6495ED" />} label="Серверы" secondaryLabel="Мониторинг серверов"/>
|
||||
<CustomCard link="/boilers" icon={<BuildingColor color="#6495ED" />} label="Котельные" />
|
||||
<CustomCard link="/map-test" icon={<MapFilled color="#6495ED" />} label="ИКС" secondaryLabel="Инженерно-картографическая система"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Container, Stack, Tabs } from '@mantine/core'
|
||||
import MapComponent from '../components/map/MapComponent'
|
||||
import { useEffect } from 'react'
|
||||
import { initializeObjectsState } from '../store/objects'
|
||||
import { deleteMapTab, setCurrentTab, useAppStore } from '../store/app'
|
||||
import { initializeMapState, useMapStore } from '../store/map'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useEffect } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Tab, TabList } from "@fluentui/react-tabs";
|
||||
import MapComponent from "../components/map/MapComponent";
|
||||
|
||||
import {
|
||||
useAppStore,
|
||||
setCurrentTab,
|
||||
deleteMapTab,
|
||||
} from "../store/app";
|
||||
import { initializeMapState, useMapStore } from "../store/map";
|
||||
import { initializeObjectsState } from "../store/objects";
|
||||
|
||||
function MapTest() {
|
||||
const { mapTab, currentTab } = useAppStore()
|
||||
const { id } = useMapStore()
|
||||
const { mapTab, currentTab } = useAppStore();
|
||||
const { id } = useMapStore();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@@ -18,65 +23,65 @@ function MapTest() {
|
||||
district: 146,
|
||||
},
|
||||
// {
|
||||
// id: uuidv4(),
|
||||
// year: 2023,
|
||||
// region: 11,
|
||||
// district: 146,
|
||||
// id: uuidv4(),
|
||||
// year: 2023,
|
||||
// region: 11,
|
||||
// district: 146,
|
||||
// },
|
||||
]
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
tabs.map(tab => useAppStore.setState((state) => {
|
||||
initializeObjectsState(tab.id, tab.region, tab.district, null, tab.year)
|
||||
initializeMapState(tab.id)
|
||||
tabs.forEach((tab) => {
|
||||
useAppStore.setState((state) => {
|
||||
initializeObjectsState(tab.id, tab.region, tab.district, null, tab.year);
|
||||
initializeMapState(tab.id);
|
||||
|
||||
return {
|
||||
mapTab: {
|
||||
...state.mapTab,
|
||||
[tab.id]: {
|
||||
year: tab.year,
|
||||
region: tab.region,
|
||||
district: tab.district
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
return {
|
||||
mapTab: {
|
||||
...state.mapTab,
|
||||
[tab.id]: {
|
||||
year: tab.year,
|
||||
region: tab.region,
|
||||
district: tab.district,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
setCurrentTab(tabs[0].id)
|
||||
setCurrentTab(tabs[0].id);
|
||||
|
||||
return () => {
|
||||
tabs.map(tab => deleteMapTab(tab.id))
|
||||
}
|
||||
}, [])
|
||||
tabs.forEach((tab) => deleteMapTab(tab.id));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container fluid w='100%' pos='relative' p={0}>
|
||||
<Tabs h='100%' variant='default' value={currentTab} onChange={setCurrentTab} keepMounted={true}>
|
||||
<Stack gap={0} h='100%'>
|
||||
<Tabs.List>
|
||||
{Object.entries(mapTab).map(([key]) => (
|
||||
<Tabs.Tab value={key} key={key}>
|
||||
{id[key]?.mapLabel}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
<div style={{ height: "100%", width: "100%", position: "relative" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<TabList
|
||||
selectedValue={currentTab}
|
||||
onTabSelect={(_, data) => setCurrentTab(data.value as string)}
|
||||
>
|
||||
{Object.entries(mapTab).map(([key]) => (
|
||||
<Tabs.Panel value={key} key={key} h='100%' pos='relative'>
|
||||
<MapComponent
|
||||
key={key}
|
||||
id={key}
|
||||
active={currentTab === key}
|
||||
/>
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tab value={key} key={key}>
|
||||
{id[key]?.mapLabel ?? `Tab ${key}`}
|
||||
</Tab>
|
||||
))}
|
||||
</Stack>
|
||||
</TabList>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Container>
|
||||
)
|
||||
<div style={{ flexGrow: 1, position: "relative" }}>
|
||||
{Object.entries(mapTab).map(([key]) =>
|
||||
currentTab === key ? (
|
||||
<div key={key} style={{ height: "100%", position: "relative" }}>
|
||||
<MapComponent key={key} id={key} active={true} />
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapTest
|
||||
export default MapTest;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card } from '@fluentui/react-components';
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Flex } from '@mantine/core';
|
||||
|
||||
function CardComponent({
|
||||
url,
|
||||
@@ -7,10 +7,10 @@ function CardComponent({
|
||||
}: { url: string, is_alive: boolean }) {
|
||||
return (
|
||||
<Card>
|
||||
<Flex p='sm' direction='column'>
|
||||
<div>
|
||||
<p>{url}</p>
|
||||
<p>{JSON.stringify(is_alive)}</p>
|
||||
</Flex>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -38,11 +38,15 @@ export default function MonitorPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
{servers.length > 0 && servers.map((server: { name: string, status: boolean }) => (
|
||||
<CardComponent url={server.name} is_alive={server.status} />
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import { Text } from "@fluentui/react-components";
|
||||
import { makeStyles } from "@fluentui/react-components";
|
||||
import { IconError404 } from "@tabler/icons-react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default function NotFound() {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Flex w={'100%'} h={'100%'} p='sm' gap='sm' align='center' justify='center'>
|
||||
<Flex direction='column' gap='sm' align='center'>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
<div className={classes.root}>
|
||||
<IconError404 size={100} />
|
||||
<Text size="xl" fw={500} ta='center'>
|
||||
<Text size={500} weight='medium' align='center'>
|
||||
Запрашиваемая страница не найдена.
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ActionIcon, Button, Flex, Group, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useEffect, useState } from "react"
|
||||
import createReport, { listCommands } from 'docx-templates'
|
||||
import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone'
|
||||
import { IconFileTypeDocx, IconPlus, IconUpload, IconX } from "@tabler/icons-react"
|
||||
import { CommandSummary } from "docx-templates/lib/types"
|
||||
import { Control, Controller, FieldValues, SubmitHandler, useFieldArray, useForm, UseFormRegister } from "react-hook-form"
|
||||
import { Button, Field, Input, Text } from "@fluentui/react-components"
|
||||
|
||||
const xslTemplate = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="1.0" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
@@ -1024,11 +1024,11 @@ const FormLoop = ({
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack align="center">
|
||||
<Stack w='100%' key={command.code}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }} key={command.code}>
|
||||
{
|
||||
fields.map((field, index) => (
|
||||
<Flex w='100%' justify='space-between' align='flex-end' key={field.id}>
|
||||
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'flex-end' }} key={field.id}>
|
||||
{command.children &&
|
||||
command.children.map(c =>
|
||||
renderCommand(
|
||||
@@ -1040,27 +1040,25 @@ const FormLoop = ({
|
||||
`${command.code}.${index}.${c.code}`
|
||||
)
|
||||
)}
|
||||
<Button variant='subtle' onClick={() => {
|
||||
<Button appearance='subtle' onClick={() => {
|
||||
remove(index)
|
||||
}}>
|
||||
<IconX />
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<ActionIcon onClick={() => {
|
||||
<Button icon={<IconPlus />} onClick={() => {
|
||||
if (command.children) {
|
||||
append(command.children.map(c => c.code).reduce((acc, key) => {
|
||||
acc[key] = '';
|
||||
return acc;
|
||||
}, {} as Record<string, string>))
|
||||
}
|
||||
}}>
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
</Stack>
|
||||
}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1074,18 +1072,17 @@ const renderCommand = (
|
||||
) => {
|
||||
if (command.type === 'INS') {
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
key={key}
|
||||
{...register(name)}
|
||||
/>
|
||||
<Field label={label}
|
||||
key={key}>
|
||||
<Input {...register(name)} />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
if (command.type === 'IMAGE') {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Text size='sm' fw={500}>{command.code}</Text>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Text size={200} weight="semibold">{command.code}</Text>
|
||||
<Controller
|
||||
key={key}
|
||||
name={name}
|
||||
@@ -1108,7 +1105,7 @@ const renderCommand = (
|
||||
}}
|
||||
maxFiles={1}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '2rem', minHeight: '220px', pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
@@ -1120,15 +1117,15 @@ const renderCommand = (
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
<Text size={300}>
|
||||
Перетащите файлы сюда или нажмите, чтобы выбрать их
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
</Dropzone>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1240,21 +1237,21 @@ const TemplateForm = ({
|
||||
if (commandList) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{commandList.map(command => {
|
||||
if (command.type === 'FOR') {
|
||||
return (
|
||||
<Stack gap={0} key={command.code}>
|
||||
<Text size='sm' fw={500}>{command.code}</Text>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }} key={command.code}>
|
||||
<Text size={200} weight='semibold'>{command.code}</Text>
|
||||
<FormLoop control={control} register={register} command={command} />
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return renderCommand(control, register, command, command.code, command.code, command.code)
|
||||
}
|
||||
})}
|
||||
<Button ml='auto' w='fit-content' type='submit'>Сохранить</Button>
|
||||
</Stack>
|
||||
<Button style={{ marginLeft: 'auto', width: 'fit-content' }} type='submit'>Сохранить</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1262,13 +1259,13 @@ const TemplateForm = ({
|
||||
|
||||
const PrintReport = () => {
|
||||
return (
|
||||
<Stack p='sm' gap='sm' w='100%'>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', padding: '1rem', gap: '1rem', width: '100%' }}>
|
||||
<TemplateForm templateUrl="/template_table.docx" />
|
||||
|
||||
<Flex gap='sm'>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<Button onClick={handleGenerateExcel}>Сохранить в Excel</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useCities, useReport, useReportExport } from "../hooks/swrHooks"
|
||||
import { useDebounce } from "@uidotdev/usehooks"
|
||||
import { ICity } from "../interfaces/fuel"
|
||||
import { mutate } from "swr"
|
||||
import { ActionIcon, Autocomplete, Badge, Button, CloseButton, Flex, ScrollAreaAutosize, Table } from "@mantine/core"
|
||||
import { IconRefresh } from "@tabler/icons-react"
|
||||
import { Badge, Button, Combobox, createTableColumn, DataGrid, DataGridBody, DataGridCell, DataGridHeader, DataGridHeaderCell, DataGridRow, Option, TableCellLayout, TableColumnDefinition } from "@fluentui/react-components"
|
||||
|
||||
export default function Reports() {
|
||||
const [download, setDownload] = useState(false)
|
||||
@@ -40,99 +40,125 @@ export default function Reports() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
|
||||
<Flex component="form" gap={'sm'}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<form style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem'
|
||||
}}>
|
||||
{/* <SearchableSelect /> */}
|
||||
<Autocomplete
|
||||
placeholder="Населенный пункт"
|
||||
flex={'1'}
|
||||
data={cities ? cities.map((item: ICity) => ({ label: item.name, value: item.id.toString() })) : []}
|
||||
onSelect={(e) => console.log(e.currentTarget.value)}
|
||||
onChange={(value) => setSearch(value)}
|
||||
onOptionSubmit={(value) => setSelectedOption(Number(value))}
|
||||
rightSection={
|
||||
search !== '' && (
|
||||
<CloseButton
|
||||
size="sm"
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
setSearch('')
|
||||
setSelectedOption(null)
|
||||
}}
|
||||
aria-label="Clear value"
|
||||
/>
|
||||
)
|
||||
}
|
||||
value={search}
|
||||
/>
|
||||
<Combobox clearable placeholder="Населенный пункт" onOptionSelect={(_, data) => {
|
||||
setSelectedOption(Number(data.optionValue))
|
||||
setSearch(data.optionText ?? "")
|
||||
}} value={search} onChange={(e) => setSearch(e.currentTarget.value)}>
|
||||
{cities && Array.isArray(cities) && cities.map((item: ICity) => (
|
||||
<Option key={item.id} value={item.id.toString()}>
|
||||
{item.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
|
||||
<ActionIcon size='auto' variant='transparent' onClick={() => refreshReport()}>
|
||||
<IconRefresh />
|
||||
</ActionIcon>
|
||||
<Button icon={<IconRefresh />} appearance="subtle" onClick={() => refreshReport()}>
|
||||
|
||||
</Button>
|
||||
|
||||
<Button disabled={!selectedOption} onClick={() => exportReport()}>
|
||||
Экспорт
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
|
||||
{report &&
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{[
|
||||
{ field: 'id', headerName: '№', width: 70 },
|
||||
...Object.keys(report).map(key => ({
|
||||
field: key,
|
||||
headerName: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
width: 150
|
||||
}))
|
||||
].map(column => (
|
||||
<Table.Th key={column.headerName}>{column.headerName}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{[...new Set(Object.keys(report).flatMap(key => Object.keys(report[key])))].map(id => {
|
||||
const row: Record<string, unknown> = { id: Number(id) };
|
||||
Object.keys(report).forEach(key => {
|
||||
row[key] = report[key][id];
|
||||
});
|
||||
return (<Table.Tr key={row.id as number}>
|
||||
{[
|
||||
{ field: 'id', headerName: '№', width: 70 },
|
||||
...Object.keys(report).map(key => ({
|
||||
field: key,
|
||||
headerName: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
width: 150
|
||||
}))
|
||||
].map(column => {
|
||||
if (column.field === 'Активность') {
|
||||
return (
|
||||
row['Активность'] ? (
|
||||
<Table.Td key={`${row.id}-${column.headerName}`}>
|
||||
<Badge fullWidth variant="light">
|
||||
Активен
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{report &&
|
||||
<ReportTable report={report} />
|
||||
}
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<Table.Td key={`${row.id}-${column.headerName}`}>
|
||||
<Badge color="gray" fullWidth variant="light">
|
||||
Отключен
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
)
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Table.Td key={`${row.id}-${column.headerName}`}>{row[column.field] as string}</Table.Td>
|
||||
)
|
||||
})}
|
||||
</Table.Tr>)
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
}
|
||||
</ScrollAreaAutosize>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface ReportType {
|
||||
[key: string]: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function ReportTable({ report }: { report: ReportType }) {
|
||||
// Build column definitions
|
||||
const columns: TableColumnDefinition<any>[] = [
|
||||
createTableColumn({
|
||||
columnId: "id",
|
||||
renderHeaderCell: () => {
|
||||
return "№"
|
||||
},
|
||||
renderCell: (item) => <TableCellLayout>{item.id}</TableCellLayout>,
|
||||
}),
|
||||
...Object.keys(report).map((key) =>
|
||||
createTableColumn({
|
||||
columnId: key,
|
||||
renderHeaderCell: () => {
|
||||
return key.charAt(0).toUpperCase() + key.slice(1)
|
||||
},
|
||||
renderCell: (item: any) => {
|
||||
if (key === "Активность") {
|
||||
return (
|
||||
<TableCellLayout>
|
||||
{item["Активность"] ? (
|
||||
<Badge color="success">Активен</Badge>
|
||||
) : (
|
||||
<Badge color="danger">Отключен</Badge>
|
||||
)}
|
||||
</TableCellLayout>
|
||||
);
|
||||
}
|
||||
return <TableCellLayout>{item[key] as string}</TableCellLayout>;
|
||||
},
|
||||
})
|
||||
),
|
||||
];
|
||||
|
||||
// Build rows from report (same logic you used)
|
||||
const items = [...new Set(Object.keys(report).flatMap((key) => Object.keys(report[key])))]
|
||||
.map((id) => {
|
||||
const row: Record<string, unknown> = { id: Number(id) };
|
||||
Object.keys(report).forEach((key) => {
|
||||
row[key] = report[key][id];
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
items={items}
|
||||
columns={columns}
|
||||
sortable
|
||||
focusMode='row_unstable'
|
||||
resizableColumns
|
||||
resizableColumnsOptions={{ autoFitColumns: false }}
|
||||
size='extra-small'
|
||||
|
||||
>
|
||||
<DataGridHeader>
|
||||
<DataGridRow>
|
||||
{({ renderHeaderCell }) => (
|
||||
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
|
||||
)}
|
||||
</DataGridRow>
|
||||
</DataGridHeader>
|
||||
|
||||
<DataGridBody>
|
||||
{({ item, rowId }) => (
|
||||
<DataGridRow key={rowId}>
|
||||
{({ renderCell }) => <DataGridCell>{renderCell(item)}</DataGridCell>}
|
||||
</DataGridRow>
|
||||
)}
|
||||
</DataGridBody>
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useRoles } from '../hooks/swrHooks'
|
||||
import { CreateField } from '../interfaces/create'
|
||||
import RoleService from '../services/RoleService'
|
||||
import { Loader, Stack } from '@mantine/core'
|
||||
import CustomTable from '../components/CustomTable'
|
||||
import { Spinner } from '@fluentui/react-components'
|
||||
|
||||
export default function Roles() {
|
||||
const { roles, isError, isLoading } = useRoles()
|
||||
@@ -13,30 +13,34 @@ export default function Roles() {
|
||||
]
|
||||
|
||||
if (isError) return <div>Произошла ошибка при получении данных.</div>
|
||||
if (isLoading) return <Loader />
|
||||
if (isLoading) return <Spinner />
|
||||
|
||||
return (
|
||||
<Stack w={'100%'} h={'100%'} p='sm'>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '1rem'
|
||||
}} >
|
||||
<CustomTable
|
||||
createFields={createFields}
|
||||
submitHandler={RoleService.createRole}
|
||||
data={roles} columns={[
|
||||
{
|
||||
accessorKey: 'id',
|
||||
name: 'id',
|
||||
header: 'id',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
name: 'name',
|
||||
header: 'Название',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
name: 'description',
|
||||
header: 'Описание',
|
||||
cell: (info) => info.getValue(),
|
||||
type: 'string'
|
||||
},
|
||||
]} />
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,46 +3,52 @@ import ServersView from "../components/ServersView"
|
||||
import ServerIpsView from "../components/ServerIpsView"
|
||||
import ServerHardware from "../components/ServerHardware"
|
||||
import ServerStorage from "../components/ServerStorages"
|
||||
import { Flex, ScrollAreaAutosize, Tabs } from "@mantine/core"
|
||||
import { Tab, TabList } from "@fluentui/react-components"
|
||||
|
||||
export default function Servers() {
|
||||
const [currentTab, setCurrentTab] = useState<string | null>('0')
|
||||
const tabs = [{
|
||||
id: 'servers',
|
||||
name: 'Серверы',
|
||||
content: <ServersView />
|
||||
},
|
||||
{
|
||||
id: 'ips',
|
||||
name: 'IP-адреса',
|
||||
content: <ServerIpsView />
|
||||
},
|
||||
{
|
||||
id: 'hardware',
|
||||
name: 'Hardware',
|
||||
content: <ServerHardware />
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
name: 'Хранилище',
|
||||
content: <ServerStorage />
|
||||
}
|
||||
]
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<string | unknown>(tabs[0].id)
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize w={'100%'} h={'100%'} p='sm'>
|
||||
<Flex gap='sm' direction='column'>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="0">Серверы</Tabs.Tab>
|
||||
<Tabs.Tab value="1">IP-адреса</Tabs.Tab>
|
||||
<Tabs.Tab value="3">Hardware</Tabs.Tab>
|
||||
<Tabs.Tab value="4">Storages</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
gap: '1rem',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<TabList selectedValue={selectedTab} onTabSelect={(_, data) => setSelectedTab(data.value)}>
|
||||
{tabs.map(tab => (
|
||||
<Tab value={tab.id}>{tab.name}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
<Tabs.Panel value="0" pt='sm'>
|
||||
<ServersView />
|
||||
</Tabs.Panel>
|
||||
<div>
|
||||
{tabs.find(tab => tab.id === selectedTab)?.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Panel value="1" pt='sm'>
|
||||
<ServerIpsView />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="2" pt='sm'>
|
||||
<ServerHardware />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="3" pt='sm'>
|
||||
<ServerStorage />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
|
||||
{/* <BarChart
|
||||
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
|
||||
series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]}
|
||||
width={500}
|
||||
height={300}
|
||||
/> */}
|
||||
</ScrollAreaAutosize>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { CreateField } from "../interfaces/create"
|
||||
import { IUser } from "../interfaces/user"
|
||||
import FormFields from "../components/FormFields"
|
||||
import AuthService from "../services/AuthService"
|
||||
import { Flex, ScrollAreaAutosize } from "@mantine/core"
|
||||
|
||||
export default function Settings() {
|
||||
const { token } = useAuthStore()
|
||||
@@ -39,13 +38,20 @@ export default function Settings() {
|
||||
]
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
p='sm'
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '1rem'
|
||||
}}
|
||||
>
|
||||
{currentUser &&
|
||||
<Flex direction='column' gap='sm' w='100%'>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<FormFields
|
||||
fields={profileFields}
|
||||
defaultValues={currentUser}
|
||||
@@ -61,8 +67,8 @@ export default function Settings() {
|
||||
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
|
||||
title="Смена пароля"
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
}
|
||||
</ScrollAreaAutosize>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import { IRole } from "../interfaces/role"
|
||||
import { useEffect, useState } from "react"
|
||||
import { CreateField } from "../interfaces/create"
|
||||
import UserService from "../services/UserService"
|
||||
import { Flex, Loader, Stack } from "@mantine/core"
|
||||
import CustomTable from "../components/CustomTable"
|
||||
import { Spinner } from "@fluentui/react-components"
|
||||
import { IUser } from "../interfaces/user"
|
||||
|
||||
export default function Users() {
|
||||
const { users, isError, isLoading } = useUsers()
|
||||
@@ -13,12 +14,20 @@ export default function Users() {
|
||||
|
||||
const [roleOptions, setRoleOptions] = useState<{ label: string, value: string }[]>()
|
||||
|
||||
const [data, setData] = useState<IUser[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(roles)) {
|
||||
setRoleOptions(roles.map((role: IRole) => ({ label: role.name, value: role.id.toString() })))
|
||||
}
|
||||
}, [roles])
|
||||
|
||||
useEffect(() => {
|
||||
if (users) {
|
||||
setData(users)
|
||||
}
|
||||
}, [users])
|
||||
|
||||
const createFields: CreateField[] = [
|
||||
{ key: 'email', headerName: 'E-mail', type: 'string', required: true, defaultValue: '' },
|
||||
{ key: 'login', headerName: 'Логин', type: 'string', required: true, defaultValue: '' },
|
||||
@@ -36,57 +45,71 @@ export default function Users() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
|
||||
<Loader />
|
||||
</Flex>
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack w={'100%'} h={'100%'} p='xs'>
|
||||
{Array.isArray(roleOptions) &&
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
{Array.isArray(roleOptions) && Array.isArray(data) &&
|
||||
<CustomTable
|
||||
createFields={createFields}
|
||||
submitHandler={UserService.createUser}
|
||||
data={users}
|
||||
data={data}
|
||||
onEditCell={(rowId, columnId, value) => {
|
||||
console.log(rowId, columnId, value)
|
||||
setData((prev) =>
|
||||
prev.map((row) =>
|
||||
row.id === rowId ? { ...row, [columnId]: value } : row
|
||||
)
|
||||
)
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'email',
|
||||
name: 'email',
|
||||
header: 'E-mail',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
accessorKey: 'login',
|
||||
name: 'login',
|
||||
header: 'Логин',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
accessorKey: 'phone',
|
||||
name: 'phone',
|
||||
header: 'Телефон',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
name: 'name',
|
||||
header: 'Имя',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
accessorKey: 'surname',
|
||||
name: 'surname',
|
||||
header: 'Фамилия',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
accessorKey: 'is_active',
|
||||
name: 'is_active',
|
||||
header: 'Активен',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "boolean"
|
||||
},
|
||||
{
|
||||
accessorKey: 'role_id',
|
||||
name: 'role_id',
|
||||
header: 'Роль',
|
||||
cell: (info) => info.getValue(),
|
||||
type: "dictionary" //TODO: dictionary getter by id
|
||||
}
|
||||
]} />
|
||||
}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import AuthService from '../../services/AuthService';
|
||||
import { Button, Flex, Loader, Paper, Text, TextInput, Transition } from '@mantine/core';
|
||||
import { IconCheck } from '@tabler/icons-react';
|
||||
import { Button, Input, Spinner, Text } from '@fluentui/react-components';
|
||||
|
||||
interface PasswordResetProps {
|
||||
email: string;
|
||||
@@ -11,7 +11,7 @@ interface PasswordResetProps {
|
||||
function PasswordReset() {
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const { register, handleSubmit, watch, setError, formState: { errors, isSubmitting } } = useForm<PasswordResetProps>({
|
||||
const { register, handleSubmit, watch, setError, formState: { isSubmitting } } = useForm<PasswordResetProps>({
|
||||
defaultValues: {
|
||||
email: ''
|
||||
}
|
||||
@@ -31,65 +31,81 @@ function PasswordReset() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<Text size="xl" fw={500}>
|
||||
Восстановление пароля
|
||||
</Text>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
margin: 'auto',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
height: 'min-content',
|
||||
borderRadius: '1rem',
|
||||
border: '1px solid #00000030',
|
||||
padding: '2rem'
|
||||
}}>
|
||||
<Text size={600} weight='medium'>
|
||||
Восстановление пароля
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!success &&
|
||||
<Transition mounted={!success} transition='fade'>
|
||||
{(styles) =>
|
||||
<Flex style={styles} direction='column' gap={'md'}>
|
||||
<Text>
|
||||
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
|
||||
</Text>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!success &&
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<Text>
|
||||
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label='E-mail'
|
||||
required
|
||||
{...register('email', { required: 'Введите E-mail' })}
|
||||
error={errors.email?.message}
|
||||
/>
|
||||
<Input
|
||||
placeholder='E-mail'
|
||||
required
|
||||
{...register('email', { required: 'Введите E-mail' })}
|
||||
//error={errors.email?.message}
|
||||
/>
|
||||
|
||||
<Flex gap='sm'>
|
||||
<Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
|
||||
{isSubmitting ? <Loader size={16} /> : 'Восстановить пароль'}
|
||||
</Button>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Button type="submit" disabled={isSubmitting || watch('email').length == 0} appearance='primary'>
|
||||
{isSubmitting ? <Spinner /> : 'Восстановить пароль'}
|
||||
</Button>
|
||||
|
||||
<Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
|
||||
Назад
|
||||
</Button>
|
||||
</Flex>
|
||||
<Button as='a' href="/auth/signin" type="button" appearance='subtle'>
|
||||
Назад
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</Flex>
|
||||
}
|
||||
|
||||
</Transition>
|
||||
}
|
||||
{success &&
|
||||
<Transition mounted={!success} transition='scale'>
|
||||
{(styles) =>
|
||||
<Flex style={styles} direction='column' gap='sm'>
|
||||
<Flex align='center' gap='sm'>
|
||||
<IconCheck />
|
||||
<Text>
|
||||
На указанный адрес было отправлено письмо с новыми данными для авторизации.
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex gap='sm'>
|
||||
<Button component='a' href="/auth/signin" type="button">
|
||||
Войти
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
</Transition>
|
||||
}
|
||||
</form>
|
||||
</Flex>
|
||||
</Paper>
|
||||
</div>
|
||||
}
|
||||
{success &&
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<IconCheck />
|
||||
<Text>
|
||||
На указанный адрес было отправлено письмо с новыми данными для авторизации.
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button as='a' href="/auth/signin" type="button">
|
||||
Войти
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { login, setUserData } from '../../store/auth';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import AuthService from '../../services/AuthService';
|
||||
import UserService from '../../services/UserService';
|
||||
import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core';
|
||||
import { Button, Field, Input, Link, Spinner, Text } from '@fluentui/react-components';
|
||||
import { pages } from '../../constants/app';
|
||||
|
||||
const SignIn = () => {
|
||||
const { register, handleSubmit, setError, formState: { errors, isSubmitting, isValid } } = useForm<LoginFormData>({
|
||||
@@ -46,53 +47,63 @@ const SignIn = () => {
|
||||
message: (err as { detail: string })?.detail
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<Text size="xl" fw={500}>
|
||||
Вход
|
||||
</Text>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
margin: 'auto',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
height: 'min-content',
|
||||
borderRadius: '1rem',
|
||||
border: '1px solid #00000030',
|
||||
padding: '2rem'
|
||||
}}>
|
||||
<Text align='center' size={500} weight='bold'>
|
||||
Вход
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<TextInput
|
||||
label='Логин'
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
|
||||
<Field label={'Логин'} validationState={errors.username?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('username', { required: 'Введите логин' })}
|
||||
error={errors.username?.message}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<TextInput
|
||||
label='Пароль'
|
||||
type='password'
|
||||
<Field label={'Пароль'} validationState={errors.password?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
type='password'
|
||||
{...register('password', { required: 'Введите пароль' })}
|
||||
error={errors.password?.message}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Flex justify='flex-end' gap='sm'>
|
||||
<Button component='a' href='/auth/password-reset' variant='transparent'>
|
||||
Восстановить пароль
|
||||
</Button>
|
||||
</Flex>
|
||||
<Link href='/auth/password-reset'>
|
||||
Восстановить пароль
|
||||
</Link>
|
||||
|
||||
<Flex gap='sm'>
|
||||
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
|
||||
{isSubmitting ? <Loader size={16} /> : 'Вход'}
|
||||
</Button>
|
||||
<Button disabled={!isValid} type="submit" appearance='primary' icon={isSubmitting ? <Spinner size='extra-tiny' /> : undefined}>
|
||||
Вход
|
||||
</Button>
|
||||
|
||||
{/* <Button component='a' flex={1} href='/auth/signup' type="button" variant='light'>
|
||||
Регистрация
|
||||
</Button> */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</Flex>
|
||||
</Paper>
|
||||
{pages.find(page => page.path === '/auth/signup')?.enabled &&
|
||||
<Button as='a' href='/auth/signup' type="button" appearance='subtle'>
|
||||
Регистрация
|
||||
</Button>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import UserService from '../../services/UserService';
|
||||
import { IUser } from '../../interfaces/user';
|
||||
import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core';
|
||||
import { Button, Field, Input, Spinner, Text } from '@fluentui/react-components';
|
||||
|
||||
const SignUp = () => {
|
||||
const { register, handleSubmit, formState: { errors, isValid, isSubmitting } } = useForm<IUser>({
|
||||
@@ -26,66 +26,76 @@ const SignUp = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<Text size="xl" fw={500}>
|
||||
Регистрация
|
||||
</Text>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
margin: 'auto',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
height: 'min-content',
|
||||
borderRadius: '1rem',
|
||||
border: '1px solid #00000030',
|
||||
padding: '2rem'
|
||||
}}>
|
||||
<Text align='center' size={500} weight='bold'>
|
||||
Регистрация
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Flex direction='column' gap='sm'>
|
||||
<TextInput
|
||||
label='Email'
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<Field label={'Email'} validationState={errors.email?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('email', { required: 'Email обязателен' })}
|
||||
error={errors.email?.message}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<TextInput
|
||||
label='Логин'
|
||||
<Field label={'Логин'} validationState={errors.login?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('login', { required: 'Логин обязателен' })}
|
||||
error={errors.login?.message}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<TextInput
|
||||
label='Телефон'
|
||||
<Field label={'Телефон'} validationState={errors.phone?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('phone')}
|
||||
error={errors.phone?.message}
|
||||
{...register('phone', { required: 'Телефон обязателен' })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<TextInput
|
||||
label='Имя'
|
||||
<Field label={'Имя'} validationState={errors.name?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('name')}
|
||||
error={errors.name?.message}
|
||||
{...register('name', { required: 'Имя обязательно' })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<TextInput
|
||||
label='Фамилия'
|
||||
<Field label={'Фамилия'} validationState={errors.surname?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('surname')}
|
||||
error={errors.surname?.message}
|
||||
{...register('surname', { required: 'Фамилия обязательна' })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<TextInput
|
||||
label='Пароль'
|
||||
type="password"
|
||||
<Field label={'Пароль'} validationState={errors.password?.message ? 'error' : 'none'}>
|
||||
<Input
|
||||
required
|
||||
{...register('password', { required: 'Пароль обязателен' })}
|
||||
error={errors.password?.message}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Flex gap='sm'>
|
||||
<Button disabled={!isValid} type="submit" flex={1} variant='filled'>
|
||||
{isSubmitting ? <Loader size={16} /> : 'Зарегистрироваться'}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</Flex>
|
||||
</Paper>
|
||||
<Button disabled={!isValid} type="submit" appearance='primary'>
|
||||
{isSubmitting ? <Spinner /> : 'Зарегистрироваться'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user