mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-10-29 09:42:23 +00:00
[WIFI-12492] Entity and venue page rework
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
@@ -3,3 +3,7 @@ build
|
|||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
.github
|
.github
|
||||||
|
docker-entrypoint.d
|
||||||
|
helm
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wlan-cloud-owprov-ui",
|
"name": "wlan-cloud-owprov-ui",
|
||||||
"version": "2.9.0(18)",
|
"version": "2.10.0(2)",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wlan-cloud-owprov-ui",
|
"name": "wlan-cloud-owprov-ui",
|
||||||
"version": "2.9.0(18)",
|
"version": "2.10.0(2)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.11",
|
"@chakra-ui/icons": "^2.0.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wlan-cloud-owprov-ui",
|
"name": "wlan-cloud-owprov-ui",
|
||||||
"version": "2.9.0(18)",
|
"version": "2.10.0(2)",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.tsx",
|
"main": "index.tsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, useStyleConfig } from '@chakra-ui/react';
|
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||||
import { ThemeProps } from 'models/Theme';
|
|
||||||
|
|
||||||
interface Props extends ThemeProps {
|
interface Props extends LayoutProps, SpaceProps {
|
||||||
variant?: string;
|
variant?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -11,13 +10,7 @@ const defaultProps = {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CardHeader = (
|
const CardHeader = ({ variant, children, ...rest }: Props) => {
|
||||||
{
|
|
||||||
variant,
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const styles = useStyleConfig('CardHeader', { variant });
|
const styles = useStyleConfig('CardHeader', { variant });
|
||||||
// Pass the computed styles into the `__css` prop
|
// Pass the computed styles into the `__css` prop
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const LocationPickerCreator = ({ locationName, createLocationName, editing, isMo
|
|||||||
{ value: 'CREATE_NEW', label: getCreateLabel() },
|
{ value: 'CREATE_NEW', label: getCreateLabel() },
|
||||||
...getOptions(),
|
...getOptions(),
|
||||||
]}
|
]}
|
||||||
w={256}
|
w="unset"
|
||||||
/>
|
/>
|
||||||
{location === 'CREATE_NEW' && newLocation && !isModal && <Form name={createLocationName} />}
|
{location === 'CREATE_NEW' && newLocation && !isModal && <Form name={createLocationName} />}
|
||||||
{location === 'CREATE_NEW' && isModal && (
|
{location === 'CREATE_NEW' && isModal && (
|
||||||
|
|||||||
@@ -4,27 +4,25 @@ import PropTypes from 'prop-types';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import CreateVenueForm from './Form';
|
import CreateVenueForm from './Form';
|
||||||
import CloseButton from 'components/Buttons/CloseButton';
|
import CloseButton from 'components/Buttons/CloseButton';
|
||||||
import CreateButton from 'components/Buttons/CreateButton';
|
|
||||||
import SaveButton from 'components/Buttons/SaveButton';
|
import SaveButton from 'components/Buttons/SaveButton';
|
||||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||||
import ModalHeader from 'components/Modals/ModalHeader';
|
import ModalHeader from 'components/Modals/ModalHeader';
|
||||||
import useFormRef from 'hooks/useFormRef';
|
import useFormRef from 'hooks/useFormRef';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
parentId: PropTypes.string,
|
parentId: PropTypes.string,
|
||||||
entityId: PropTypes.string,
|
entityId: PropTypes.string,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
isDisabled: false,
|
|
||||||
parentId: '',
|
parentId: '',
|
||||||
entityId: '',
|
entityId: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateVenueModal = ({ parentId, entityId, isDisabled }) => {
|
const CreateVenueModal = ({ isOpen, onClose, parentId, entityId }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||||
const { form, formRef } = useFormRef();
|
const { form, formRef } = useFormRef();
|
||||||
|
|
||||||
@@ -36,37 +34,35 @@ const CreateVenueModal = ({ parentId, entityId, isDisabled }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
||||||
<CreateButton onClick={onOpen} isDisabled={isDisabled} />
|
<ModalOverlay />
|
||||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||||
<ModalOverlay />
|
<ModalHeader
|
||||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
title={t('crud.create_object', { obj: t('venues.sub_one') })}
|
||||||
<ModalHeader
|
right={
|
||||||
title={t('crud.create_object', { obj: t('venues.sub_one') })}
|
<>
|
||||||
right={
|
<SaveButton
|
||||||
<>
|
onClick={form.submitForm}
|
||||||
<SaveButton
|
isLoading={form.isSubmitting}
|
||||||
onClick={form.submitForm}
|
isDisabled={!form.isValid || !form.dirty}
|
||||||
isLoading={form.isSubmitting}
|
isCompact
|
||||||
isDisabled={!form.isValid || !form.dirty}
|
/>
|
||||||
/>
|
<CloseButton ml={2} onClick={closeModal} />
|
||||||
<CloseButton ml={2} onClick={closeModal} />
|
</>
|
||||||
</>
|
}
|
||||||
}
|
/>
|
||||||
|
<ModalBody>
|
||||||
|
<CreateVenueForm
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
formRef={formRef}
|
||||||
|
parentId={parentId}
|
||||||
|
entityId={entityId}
|
||||||
/>
|
/>
|
||||||
<ModalBody>
|
</ModalBody>
|
||||||
<CreateVenueForm
|
</ModalContent>
|
||||||
isOpen={isOpen}
|
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||||
onClose={onClose}
|
</Modal>
|
||||||
formRef={formRef}
|
|
||||||
parentId={parentId}
|
|
||||||
entityId={entityId}
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,248 +1,16 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
AnalyticsBoardApiResponse,
|
||||||
|
AnalyticsBoardDevicesApiResponse,
|
||||||
|
AnalyticsClientLifecycleApiResponse,
|
||||||
|
AnalyticsTimePointsApiResponse,
|
||||||
|
} from 'models/Analytics';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { Note } from 'models/Note';
|
|
||||||
import { PageInfo, SortInfo } from 'models/Table';
|
import { PageInfo, SortInfo } from 'models/Table';
|
||||||
import { axiosAnalytics } from 'utils/axiosInstances';
|
import { axiosAnalytics } from 'utils/axiosInstances';
|
||||||
|
|
||||||
export type AnalyticsBoardDevice = {
|
|
||||||
associations_2g: number;
|
|
||||||
associations_5g: number;
|
|
||||||
associations_6g: number;
|
|
||||||
boardId: string;
|
|
||||||
connected: boolean;
|
|
||||||
connectionIp: string;
|
|
||||||
deviceType: string;
|
|
||||||
health: number;
|
|
||||||
lastConnection: number;
|
|
||||||
lastContact: number;
|
|
||||||
lastDisconnection: number;
|
|
||||||
lastFirmware: string;
|
|
||||||
lastFirmwareUpdate: number;
|
|
||||||
lastHealth: number;
|
|
||||||
lastPing: number;
|
|
||||||
lastState: number;
|
|
||||||
locale: string;
|
|
||||||
memory: number;
|
|
||||||
pings: number;
|
|
||||||
serialNumber: string;
|
|
||||||
states: number;
|
|
||||||
type: string;
|
|
||||||
uptime: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsBoardDevicesApiResponse = {
|
|
||||||
devices: AnalyticsBoardDevice[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsBoardApiResponse = {
|
|
||||||
created: number;
|
|
||||||
description: string;
|
|
||||||
id: string;
|
|
||||||
modified: number;
|
|
||||||
name: string;
|
|
||||||
notes: Note[];
|
|
||||||
tags: string[];
|
|
||||||
venueList: {
|
|
||||||
description: string;
|
|
||||||
id: string;
|
|
||||||
interval: number;
|
|
||||||
monitorSubVenues: boolean;
|
|
||||||
name: string;
|
|
||||||
retention: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsClientLifecycleApiResponse = {
|
|
||||||
ack_signal: number;
|
|
||||||
ack_signal_avg: number;
|
|
||||||
active_ms: number;
|
|
||||||
bssid: string;
|
|
||||||
busy_ms: number;
|
|
||||||
channel: number;
|
|
||||||
channel_width: number;
|
|
||||||
connected: number;
|
|
||||||
inactive: number;
|
|
||||||
ipv4: string;
|
|
||||||
ipv6: string;
|
|
||||||
mode: string;
|
|
||||||
noise: number;
|
|
||||||
receive_ms: number;
|
|
||||||
rssi: number;
|
|
||||||
rx_bitrate: number;
|
|
||||||
rx_bytes: number;
|
|
||||||
rx_chwidth: number;
|
|
||||||
rx_duration: number;
|
|
||||||
rx_mcs: number;
|
|
||||||
rx_nss: number;
|
|
||||||
rx_packets: number;
|
|
||||||
rx_vht: boolean;
|
|
||||||
ssid: string;
|
|
||||||
station_id: string;
|
|
||||||
timestamp: number;
|
|
||||||
tx_bitrate: number;
|
|
||||||
tx_bytes: number;
|
|
||||||
tx_chwidth: number;
|
|
||||||
tx_duration: number;
|
|
||||||
tx_mcs: number;
|
|
||||||
tx_nss: number;
|
|
||||||
tx_packets: number;
|
|
||||||
tx_power: number;
|
|
||||||
tx_retries: number;
|
|
||||||
tx_vht: boolean;
|
|
||||||
venue_id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsApData = {
|
|
||||||
collisions: number;
|
|
||||||
multicast: number;
|
|
||||||
rx_bytes: number;
|
|
||||||
rx_bytes_bw: number;
|
|
||||||
rx_bytes_delta: number;
|
|
||||||
rx_dropped: number;
|
|
||||||
rx_dropped_delta: number;
|
|
||||||
rx_dropped_pct: number;
|
|
||||||
rx_errors: number;
|
|
||||||
rx_errors_delta: number;
|
|
||||||
rx_errors_pct: number;
|
|
||||||
rx_packets: number;
|
|
||||||
rx_packets_bw: number;
|
|
||||||
rx_packets_delta: number;
|
|
||||||
tx_bytes: number;
|
|
||||||
tx_bytes_bw: number;
|
|
||||||
tx_bytes_delta: number;
|
|
||||||
tx_dropped: number;
|
|
||||||
tx_dropped_delta: number;
|
|
||||||
tx_dropped_pct: number;
|
|
||||||
tx_errors: number;
|
|
||||||
tx_errors_delta: number;
|
|
||||||
tx_errors_pct: number;
|
|
||||||
tx_packets: number;
|
|
||||||
tx_packets_bw: number;
|
|
||||||
tx_packets_delta: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsRadioData = {
|
|
||||||
active_ms: number;
|
|
||||||
active_pct: number;
|
|
||||||
band: number;
|
|
||||||
busy_ms: number;
|
|
||||||
busy_pct: number;
|
|
||||||
channel: number;
|
|
||||||
channel_width: number;
|
|
||||||
noise: number;
|
|
||||||
receive_ms: number;
|
|
||||||
receive_pct: number;
|
|
||||||
temperature: number;
|
|
||||||
transmit_ms: number;
|
|
||||||
transmit_pct: number;
|
|
||||||
tx_power: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsAssociationData = {
|
|
||||||
connected: number;
|
|
||||||
inactive: number;
|
|
||||||
rssi: number;
|
|
||||||
rx_bytes: number;
|
|
||||||
rx_bytes_bw: number;
|
|
||||||
rx_bytes_delta: number;
|
|
||||||
rx_packets: number;
|
|
||||||
rx_packets_bw: number;
|
|
||||||
rx_packets_delta: number;
|
|
||||||
rx_rate: {
|
|
||||||
bitrate: number;
|
|
||||||
chwidth: number;
|
|
||||||
ht: boolean;
|
|
||||||
mcs: number;
|
|
||||||
nss: number;
|
|
||||||
sgi: boolean;
|
|
||||||
};
|
|
||||||
station: string;
|
|
||||||
tx_bytes: number;
|
|
||||||
tx_bytes_bw: number;
|
|
||||||
tx_bytes_delta: number;
|
|
||||||
tx_duration: number;
|
|
||||||
tx_duration_delta: number;
|
|
||||||
tx_duration_pct: number;
|
|
||||||
tx_failed: number;
|
|
||||||
tx_failed_delta: number;
|
|
||||||
tx_failed_pct: number;
|
|
||||||
tx_packets: number;
|
|
||||||
tx_packets_bw: number;
|
|
||||||
tx_packets_delta: number;
|
|
||||||
tx_rate: {
|
|
||||||
bitrate: number;
|
|
||||||
chwidth: number;
|
|
||||||
ht: boolean;
|
|
||||||
mcs: number;
|
|
||||||
nss: number;
|
|
||||||
sgi: boolean;
|
|
||||||
};
|
|
||||||
tx_retries: number;
|
|
||||||
tx_retries_delta: number;
|
|
||||||
tx_retries_pct: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsSsidData = {
|
|
||||||
associations: AnalyticsAssociationData[];
|
|
||||||
band: 2;
|
|
||||||
bssid: string;
|
|
||||||
channel: number;
|
|
||||||
mode: string;
|
|
||||||
rx_bytes_bw: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
rx_packets_bw: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
ssid: string;
|
|
||||||
tx_bytes_bw: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
tx_duration_pct: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
tx_failed_pct: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
tx_packets_bw: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
tx_retries_pct: {
|
|
||||||
avg: number;
|
|
||||||
max: number;
|
|
||||||
min: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsTimePointApiResponse = {
|
|
||||||
ap_data: AnalyticsApData;
|
|
||||||
boardId: string;
|
|
||||||
device_info: AnalyticsBoardDevice;
|
|
||||||
id: string;
|
|
||||||
radio_data: AnalyticsRadioData[];
|
|
||||||
serialNumber: string;
|
|
||||||
ssid_data: AnalyticsSsidData[];
|
|
||||||
timestamp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsTimePointsApiResponse = {
|
|
||||||
points: AnalyticsTimePointApiResponse[][];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetAnalyticsBoard = ({ id }: { id?: string }) => {
|
export const useGetAnalyticsBoard = ({ id }: { id?: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -344,26 +112,6 @@ export const useGetClientLifecycleTableSpecs = () =>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const getPartialClients = async (venueId: string, offset: number) =>
|
|
||||||
axiosAnalytics
|
|
||||||
.get(`wifiClientHistory?macsOnly=true&venue=${venueId}&limit=500&offset=${offset}`)
|
|
||||||
.then(({ data }) => data.entries as string[]);
|
|
||||||
|
|
||||||
export const getAllClients = async (venueId: string) => {
|
|
||||||
const allClients: string[] = [];
|
|
||||||
let continueFirmware = true;
|
|
||||||
let offset = 0;
|
|
||||||
while (continueFirmware) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const newClients = await getPartialClients(venueId, offset);
|
|
||||||
if (newClients === null || newClients.length === 0 || newClients.length < 500 || offset >= 50000)
|
|
||||||
continueFirmware = false;
|
|
||||||
allClients.push(...newClients);
|
|
||||||
offset += 500;
|
|
||||||
}
|
|
||||||
return allClients;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetClientLifecycleCount = ({
|
export const useGetClientLifecycleCount = ({
|
||||||
venueId,
|
venueId,
|
||||||
mac,
|
mac,
|
||||||
@@ -497,9 +245,31 @@ export const useGetAnalyticsBoardTimepoints = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateAnalyticsBoard = () => useMutation((newBoard) => axiosAnalytics.post('board/0', newBoard));
|
export const useCreateAnalyticsBoard = () =>
|
||||||
|
useMutation((newBoard: unknown) => axiosAnalytics.post('board/0', newBoard));
|
||||||
|
|
||||||
export const useUpdateAnalyticsBoard = () =>
|
export const useUpdateAnalyticsBoard = ({ id }: { id: string }) => {
|
||||||
useMutation((newBoard: { id: string }) => axiosAnalytics.put(`board/${newBoard.id}`, newBoard));
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
export const useDeleteAnalyticsBoard = () => useMutation((id) => axiosAnalytics.delete(`board/${id}`, {}));
|
return useMutation(
|
||||||
|
(newBoard: {
|
||||||
|
name: string;
|
||||||
|
venueList: [
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
retention: number;
|
||||||
|
interval: number;
|
||||||
|
monitorSubVenues: boolean;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}) => axiosAnalytics.put(`board/${id}`, newBoard),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['get-board', id]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAnalyticsBoard = () => useMutation((id: string) => axiosAnalytics.delete(`board/${id}`, {}));
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useDefaultPage from 'hooks/useDefaultPage';
|
import { Entity } from '../../models/Entity';
|
||||||
|
import useDefaultPage from '../useDefaultPage';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { Entity } from 'models/Entity';
|
|
||||||
import { axiosProv, axiosSec } from 'utils/axiosInstances';
|
import { axiosProv, axiosSec } from 'utils/axiosInstances';
|
||||||
|
|
||||||
export const useGetEntityTree = () => {
|
export const useGetEntityTree = () => {
|
||||||
@@ -151,7 +151,19 @@ export const useCreateEntity = (isRoot = false) =>
|
|||||||
axiosProv.post(`entity/${isRoot ? '0000-0000-0000' : 0}`, newEnt).then(({ data }) => data as Entity),
|
axiosProv.post(`entity/${isRoot ? '0000-0000-0000' : 0}`, newEnt).then(({ data }) => data as Entity),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useUpdateEntity = ({ id }: { id: string }) =>
|
export const useUpdateEntity = ({ id }: { id: string }) => {
|
||||||
useMutation((newEnt) => axiosProv.put(`entity/${id}`, newEnt).then(({ data }) => data as Entity));
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(newEnt: Partial<Entity>) => axiosProv.put(`entity/${id}`, newEnt).then(({ data }) => data as Entity),
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries(['get-entity-tree']);
|
||||||
|
queryClient.invalidateQueries(['get-entities']);
|
||||||
|
queryClient.setQueryData(['get-entity', id], data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeleteEntity = () => useMutation((id: string) => axiosProv.delete(`entity/${id}`));
|
export const useDeleteEntity = () => useMutation((id: string) => axiosProv.delete(`entity/${id}`));
|
||||||
|
|||||||
@@ -177,4 +177,4 @@ export const useGetResource = ({ id, enabled }: { id: string; enabled: boolean }
|
|||||||
export const useCreateResource = () => useMutation((newResource: unknown) => axiosProv.post('variable/0', newResource));
|
export const useCreateResource = () => useMutation((newResource: unknown) => axiosProv.post('variable/0', newResource));
|
||||||
export const useUpdateResource = (id: string) =>
|
export const useUpdateResource = (id: string) =>
|
||||||
useMutation((resource: unknown) => axiosProv.put(`variable/${id}`, resource));
|
useMutation((resource: unknown) => axiosProv.put(`variable/${id}`, resource));
|
||||||
export const useDeleteResource = () => useMutation((id) => axiosProv.delete(`variable/${id}`, {}));
|
export const useDeleteResource = () => useMutation((id: string) => axiosProv.delete(`variable/${id}`, {}));
|
||||||
|
|||||||
@@ -4,30 +4,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import useDefaultPage from '../useDefaultPage';
|
import useDefaultPage from '../useDefaultPage';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { DeviceRules } from 'models/Basic';
|
import { VenueApiResponse } from 'models/Venue';
|
||||||
import { Note } from 'models/Note';
|
|
||||||
import { axiosProv } from 'utils/axiosInstances';
|
import { axiosProv } from 'utils/axiosInstances';
|
||||||
|
|
||||||
export interface VenueApiResponse {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
parent: string;
|
|
||||||
devices: string[];
|
|
||||||
children: string[];
|
|
||||||
contacts: string[];
|
|
||||||
entity: string;
|
|
||||||
boards: string[];
|
|
||||||
created: number;
|
|
||||||
modified: number;
|
|
||||||
configurations: string[];
|
|
||||||
notes: Note[];
|
|
||||||
variables: string[];
|
|
||||||
location: string;
|
|
||||||
sourceIP: string[];
|
|
||||||
deviceRules: DeviceRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVenuesBatch = async (limit: number, offset: number) =>
|
const getVenuesBatch = async (limit: number, offset: number) =>
|
||||||
axiosProv
|
axiosProv
|
||||||
.get(`venue?withExtendedInfo=true&offset=${offset}&limit=${limit}`)
|
.get(`venue?withExtendedInfo=true&offset=${offset}&limit=${limit}`)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: -30px;
|
margin-left: -20px;
|
||||||
|
margin-bottom: -20px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.my-masonry-grid_column {
|
.my-masonry-grid_column {
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { Box, useStyleConfig } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PanelContainer = (
|
|
||||||
{
|
|
||||||
children
|
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
const styles = useStyleConfig('PanelContainer');
|
|
||||||
// Pass the computed styles into the `__css` prop
|
|
||||||
return <Box __css={styles}>{children}</Box>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PanelContainer;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { Box, useStyleConfig } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PanelContent = (
|
|
||||||
{
|
|
||||||
children
|
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
const styles = useStyleConfig('PanelContent');
|
|
||||||
|
|
||||||
return <Box __css={styles}>{children}</Box>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PanelContent;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Box, LayoutProps, useStyleConfig } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
interface Props extends LayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MainPanel = (
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
const styles = useStyleConfig('MainPanel');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box __css={styles} {...props}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MainPanel;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ChevronDownIcon, HamburgerIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
|
import { HamburgerIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -12,73 +12,55 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
Heading,
|
Heading,
|
||||||
HStack,
|
HStack,
|
||||||
VStack,
|
|
||||||
Text,
|
Text,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
useBreakpoint,
|
||||||
|
Portal,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ArrowCircleLeft, MapTrifold } from 'phosphor-react';
|
import { ArrowCircleLeft, MapTrifold } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import LanguageSwitcher from 'components/LanguageSwitcher';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import routes from 'router/routes';
|
|
||||||
import { uppercaseFirstLetter } from 'utils/stringHelper';
|
|
||||||
|
|
||||||
interface Props {
|
export type NavbarProps = {
|
||||||
secondary: boolean;
|
|
||||||
isSidebarOpen: boolean;
|
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
}
|
activeRoute?: string;
|
||||||
|
languageSwitcher?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
const Navbar = ({ secondary, toggleSidebar, isSidebarOpen }: Props) => {
|
export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
const { colorMode, toggleColorMode } = useColorMode();
|
const { colorMode, toggleColorMode } = useColorMode();
|
||||||
const { logout, user, avatar } = useAuth();
|
const { logout, user, avatar } = useAuth();
|
||||||
const getActiveRoute = () => {
|
|
||||||
const route = routes.find(
|
|
||||||
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (route) return route.navName ?? route.name;
|
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||||
|
|
||||||
return '';
|
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
||||||
};
|
const bg = useColorModeValue(
|
||||||
|
|
||||||
// Style variables
|
|
||||||
let navbarPosition: 'absolute' | 'fixed' = 'absolute';
|
|
||||||
let navbarFilter = 'none';
|
|
||||||
let navbarBackdrop = 'blur(21px)';
|
|
||||||
let navbarShadow = 'none';
|
|
||||||
let navbarBg = 'none';
|
|
||||||
let navbarBorder = 'transparent';
|
|
||||||
let secondaryMargin = '0px';
|
|
||||||
|
|
||||||
// Values if scrolled
|
|
||||||
const scrolledNavbarShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
|
||||||
const scrolledNavbarBg = useColorModeValue(
|
|
||||||
'linear-gradient(112.83deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.8) 110.84%)',
|
'linear-gradient(112.83deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.8) 110.84%)',
|
||||||
'linear-gradient(112.83deg, rgba(255, 255, 255, 0.21) 0%, rgba(255, 255, 255, 0) 110.84%)',
|
'linear-gradient(112.83deg, rgba(255, 255, 255, 0.21) 0%, rgba(255, 255, 255, 0) 110.84%)',
|
||||||
);
|
);
|
||||||
const scrolledNavbarBorder = useColorModeValue('#FFFFFF', 'rgba(255, 255, 255, 0.31)');
|
const borderColor = useColorModeValue('#FFFFFF', 'rgba(255, 255, 255, 0.31)');
|
||||||
const scrolledNavbarFilter = useColorModeValue('none', 'drop-shadow(0px 7px 23px rgba(0, 0, 0, 0.05))');
|
const filter = useColorModeValue('none', 'drop-shadow(0px 7px 23px rgba(0, 0, 0, 0.05))');
|
||||||
|
const scrollDependentStyles = scrolled
|
||||||
if (scrolled === true) {
|
? ({
|
||||||
navbarPosition = 'fixed';
|
position: 'fixed',
|
||||||
navbarShadow = scrolledNavbarShadow;
|
boxShadow,
|
||||||
navbarBg = scrolledNavbarBg;
|
bg,
|
||||||
navbarBorder = scrolledNavbarBorder;
|
borderColor,
|
||||||
navbarFilter = scrolledNavbarFilter;
|
filter,
|
||||||
}
|
} as const)
|
||||||
|
: ({
|
||||||
if (secondary) {
|
position: 'absolute',
|
||||||
navbarBackdrop = 'none';
|
filter: 'none',
|
||||||
navbarPosition = 'absolute';
|
boxShadow: 'none',
|
||||||
secondaryMargin = '22px';
|
bg: 'none',
|
||||||
}
|
borderColor: 'transparent',
|
||||||
|
} as const);
|
||||||
|
|
||||||
const goBack = () => navigate(-1);
|
const goBack = () => navigate(-1);
|
||||||
|
|
||||||
@@ -96,103 +78,86 @@ const Navbar = ({ secondary, toggleSidebar, isSidebarOpen }: Props) => {
|
|||||||
window.addEventListener('scroll', changeNavbar);
|
window.addEventListener('scroll', changeNavbar);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Portal>
|
||||||
position={navbarPosition}
|
<Flex
|
||||||
boxShadow={navbarShadow}
|
{...scrollDependentStyles}
|
||||||
bg={navbarBg}
|
backdropFilter="blur(21px)"
|
||||||
borderColor={navbarBorder}
|
borderWidth="1.5px"
|
||||||
filter={navbarFilter}
|
borderStyle="solid"
|
||||||
backdropFilter={navbarBackdrop}
|
transitionDelay="0s, 0s, 0s, 0s"
|
||||||
borderWidth="1.5px"
|
transitionDuration=" 0.25s, 0.25s, 0.25s, 0s"
|
||||||
borderStyle="solid"
|
transition-property="box-shadow, background-color, filter, border"
|
||||||
transitionDelay="0s, 0s, 0s, 0s"
|
transitionTimingFunction="linear, linear, linear, linear"
|
||||||
transitionDuration=" 0.25s, 0.25s, 0.25s, 0s"
|
alignItems="center"
|
||||||
transition-property="box-shadow, background-color, filter, border"
|
borderRadius="15px"
|
||||||
transitionTimingFunction="linear, linear, linear, linear"
|
minH="75px"
|
||||||
alignItems="center"
|
justifyContent="center"
|
||||||
borderRadius="16px"
|
lineHeight="25.6px"
|
||||||
display="flex"
|
pb="8px"
|
||||||
minH="75px"
|
right={{ base: '0px', sm: '0px', lg: '20px' }}
|
||||||
justifyContent="center"
|
ps="12px"
|
||||||
lineHeight="25.6px"
|
pt="8px"
|
||||||
mx="auto"
|
top="15px"
|
||||||
mt={secondaryMargin}
|
w={isCompact ? '100%' : 'calc(100vw - 256px)'}
|
||||||
pb="8px"
|
>
|
||||||
right={{ base: '0px', sm: '0px' }}
|
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||||
pl="30px"
|
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
||||||
ps="12px"
|
<Heading>{activeRoute}</Heading>
|
||||||
pt="8px"
|
<Tooltip label={t('common.go_back')}>
|
||||||
top="18px"
|
<IconButton
|
||||||
w={{
|
mt={2}
|
||||||
base: '100%',
|
ml={4}
|
||||||
sm: isSidebarOpen ? 'calc(100vw - 70px - 196px)' : '100%',
|
colorScheme="blue"
|
||||||
md: isSidebarOpen ? 'calc(100vw - 70px - 196px)' : '100%',
|
aria-label={t('common.go_back')}
|
||||||
}}
|
onClick={goBack}
|
||||||
>
|
size="sm"
|
||||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
icon={<ArrowCircleLeft width={20} height={20} />}
|
||||||
<HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />
|
/>
|
||||||
<Heading>{t(getActiveRoute())}</Heading>
|
</Tooltip>
|
||||||
<Tooltip label={t('common.go_back')}>
|
<Box ms="auto" w={{ base: 'unset' }}>
|
||||||
<IconButton
|
<Flex alignItems="center" flexDirection="row">
|
||||||
mt={2}
|
<Tooltip hasArrow label={t('common.go_to_map')}>
|
||||||
ml={4}
|
<IconButton
|
||||||
colorScheme="blue"
|
aria-label={t('common.go_to_map')}
|
||||||
aria-label={t('common.go_back')}
|
variant="ghost"
|
||||||
onClick={goBack}
|
icon={<MapTrifold size={24} />}
|
||||||
icon={<ArrowCircleLeft width={20} height={20} />}
|
onClick={goToMap}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Box ms="auto" w={{ base: 'unset' }}>
|
<Tooltip hasArrow label={t('common.theme')}>
|
||||||
<Flex alignItems="center" flexDirection="row">
|
<IconButton
|
||||||
<Tooltip hasArrow label={t('common.go_to_map')}>
|
aria-label={t('common.theme')}
|
||||||
<IconButton
|
variant="ghost"
|
||||||
aria-label={t('common.go_to_map')}
|
icon={colorMode === 'light' ? <MoonIcon h="20px" w="20px" /> : <SunIcon h="20px" w="20px" />}
|
||||||
variant="ghost"
|
onClick={toggleColorMode}
|
||||||
icon={<MapTrifold size={24} />}
|
/>
|
||||||
onClick={goToMap}
|
</Tooltip>
|
||||||
/>
|
{languageSwitcher}
|
||||||
</Tooltip>
|
<HStack spacing={{ base: '0', md: '6' }} ml={1} mr={4}>
|
||||||
<Tooltip hasArrow label={t('common.theme')}>
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('common.theme')}
|
|
||||||
variant="ghost"
|
|
||||||
icon={colorMode === 'light' ? <MoonIcon h="20px" w="20px" /> : <SunIcon h="20px" w="20px" />}
|
|
||||||
onClick={toggleColorMode}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<LanguageSwitcher />
|
|
||||||
<HStack spacing={{ base: '0', md: '6' }} ml={6} mr={4}>
|
|
||||||
<Flex alignItems="center">
|
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: 'none' }}>
|
<MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: 'none' }}>
|
||||||
<HStack>
|
<HStack>
|
||||||
<VStack display={{ base: 'none', md: 'flex' }} alignItems="flex-start" spacing={0} height={12}>
|
{!isCompact && <Text fontWeight="bold">{user?.name}</Text>}
|
||||||
<Text fontWeight="bold">{user?.name}</Text>
|
<Avatar h="40px" w="40px" fontSize="0.8rem" lineHeight="2rem" src={avatar} name={user?.name} />
|
||||||
<Text fontSize="sm">{`${uppercaseFirstLetter(user?.userRole)}`}</Text>
|
|
||||||
</VStack>
|
|
||||||
<Avatar src={avatar} name={user?.name} />
|
|
||||||
<Box display={{ base: 'none', md: 'flex' }}>
|
|
||||||
<ChevronDownIcon />
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList
|
<Portal>
|
||||||
bg={useColorModeValue('white', 'gray.900')}
|
<MenuList
|
||||||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
bg={useColorModeValue('white', 'gray.900')}
|
||||||
>
|
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||||
<MenuItem onClick={goToProfile} w="100%">
|
>
|
||||||
{t('account.title')}
|
<MenuItem onClick={goToProfile} w="100%">
|
||||||
</MenuItem>
|
{t('account.title')}
|
||||||
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Portal>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Flex>
|
</HStack>
|
||||||
</HStack>
|
</Flex>
|
||||||
</Flex>
|
</Box>
|
||||||
</Box>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Navbar;
|
|
||||||
|
|||||||
48
src/layout/PageContainer/index.tsx
Normal file
48
src/layout/PageContainer/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Center, Flex, Spinner, useBreakpoint } from '@chakra-ui/react';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
|
export type PageContainerProps = {
|
||||||
|
waitForUser: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageContainer = ({ waitForUser, children }: PageContainerProps) => {
|
||||||
|
const { isUserLoaded } = useAuth();
|
||||||
|
const breakpoint = useBreakpoint('xl');
|
||||||
|
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w={isCompact ? 'calc(100%)' : 'calc(100% - 210px)'}
|
||||||
|
float="right"
|
||||||
|
position="relative"
|
||||||
|
transition="all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1)"
|
||||||
|
transitionDelay=".2s, .2s, .35s"
|
||||||
|
transitionProperty="top, bottom, width"
|
||||||
|
transitionTimingFunction="linear, linear, ease"
|
||||||
|
px="15px"
|
||||||
|
pb="15px"
|
||||||
|
>
|
||||||
|
<Box minH="calc(100vh - 123px)" pt="105px" pl="10px" pr="5px" pb="0px">
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<React.Suspense
|
||||||
|
fallback={
|
||||||
|
<Center mt="100px">
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Center>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{waitForUser && !isUserLoaded ? (
|
||||||
|
<Center mt="100px">
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</React.Suspense>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import EntityNavButton from './EntityNavButton';
|
|
||||||
import NavLinkButton from './NavLinkButton';
|
|
||||||
import { Route } from 'models/Routes';
|
|
||||||
|
|
||||||
const createLinks = (
|
|
||||||
routes: Route[],
|
|
||||||
activeRoute: (path: string, otherRoute: string | undefined) => string,
|
|
||||||
role: string,
|
|
||||||
toggleSidebar = () => {},
|
|
||||||
) =>
|
|
||||||
routes.map((route) =>
|
|
||||||
route.isEntity ? (
|
|
||||||
<EntityNavButton key={uuid()} activeRoute={activeRoute} role={role} route={route} toggleSidebar={toggleSidebar} />
|
|
||||||
) : (
|
|
||||||
<NavLinkButton key={uuid()} activeRoute={activeRoute} role={role} route={route} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default createLinks;
|
|
||||||
@@ -9,20 +9,12 @@ import { Route } from 'models/Routes';
|
|||||||
const variantChange = '0.2s linear';
|
const variantChange = '0.2s linear';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeRoute: (path: string, otherRoute: string | undefined) => string;
|
isActive: boolean;
|
||||||
route: Route;
|
route: Route;
|
||||||
role: string;
|
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntityNavButton = (
|
const EntityNavButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||||
{
|
|
||||||
activeRoute,
|
|
||||||
route,
|
|
||||||
role,
|
|
||||||
toggleSidebar
|
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const activeArrowColor = useColorModeValue('var(--chakra-colors-gray-700)', 'white');
|
const activeArrowColor = useColorModeValue('var(--chakra-colors-gray-700)', 'white');
|
||||||
@@ -33,17 +25,15 @@ const EntityNavButton = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityPopover isOpen={isOpen} onClose={onClose} toggleSidebar={toggleSidebar}>
|
<EntityPopover isOpen={isOpen} onClose={onClose} toggleSidebar={toggleSidebar}>
|
||||||
{activeRoute(route.path, '/venue/:id') === 'active' ? (
|
{isActive ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
hidden={route.hidden || !route.authorized.includes(role)}
|
|
||||||
boxSize="initial"
|
boxSize="initial"
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
boxShadow="none"
|
boxShadow="none"
|
||||||
bg="transparent"
|
bg="transparent"
|
||||||
transition={variantChange}
|
transition={variantChange}
|
||||||
mb="12px"
|
|
||||||
mx="auto"
|
mx="auto"
|
||||||
ps="10px"
|
ps="10px"
|
||||||
py="12px"
|
py="12px"
|
||||||
@@ -71,12 +61,10 @@ const EntityNavButton = (
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
hidden={route.hidden || !route.authorized.includes(role)}
|
|
||||||
boxSize="initial"
|
boxSize="initial"
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
bg="transparent"
|
bg="transparent"
|
||||||
mb="12px"
|
|
||||||
mx="auto"
|
mx="auto"
|
||||||
py="12px"
|
py="12px"
|
||||||
ps="10px"
|
ps="10px"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { TreeStructure, Buildings, X } from 'phosphor-react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import useGetEntityTree from 'hooks/Network/EntityTree';
|
import { useGetEntityTree } from 'hooks/Network/Entity';
|
||||||
|
|
||||||
interface Tree {
|
interface Tree {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -110,14 +110,7 @@ interface Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
}
|
}
|
||||||
const EntityPopover = (
|
const EntityPopover = ({ isOpen, onClose, children, toggleSidebar }: Props) => {
|
||||||
{
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
children,
|
|
||||||
toggleSidebar
|
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
@@ -126,9 +119,9 @@ const EntityPopover = (
|
|||||||
const initRef = React.useRef<HTMLButtonElement>();
|
const initRef = React.useRef<HTMLButtonElement>();
|
||||||
|
|
||||||
const goTo = useCallback(
|
const goTo = useCallback(
|
||||||
(id, type) => {
|
(id: string, type: string) => {
|
||||||
navigate(`/${type}/${id}`);
|
navigate(`/${type}/${id}`);
|
||||||
if (breakpoint === 'base') toggleSidebar();
|
if (breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md') toggleSidebar();
|
||||||
},
|
},
|
||||||
[breakpoint],
|
[breakpoint],
|
||||||
);
|
);
|
||||||
@@ -145,7 +138,7 @@ const EntityPopover = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
offset={[140, -100]}
|
offset={[0, -100]}
|
||||||
isLazy
|
isLazy
|
||||||
returnFocusOnClose={false}
|
returnFocusOnClose={false}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -154,8 +147,11 @@ const EntityPopover = (
|
|||||||
closeOnBlur={closeOnBlur}
|
closeOnBlur={closeOnBlur}
|
||||||
initialFocusRef={initRef as React.RefObject<FocusableElement>}
|
initialFocusRef={initRef as React.RefObject<FocusableElement>}
|
||||||
>
|
>
|
||||||
<PopoverAnchor>{children}</PopoverAnchor>
|
{
|
||||||
{breakpoint === 'base' ? (
|
// @ts-ignore
|
||||||
|
<PopoverAnchor>{children}</PopoverAnchor>
|
||||||
|
}
|
||||||
|
{breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md' ? (
|
||||||
<PopoverContent maxW={{ base: 'calc(60vw)' }}>
|
<PopoverContent maxW={{ base: 'calc(60vw)' }}>
|
||||||
<PopoverHeader fontWeight="semibold" display="flex" alignItems="center">
|
<PopoverHeader fontWeight="semibold" display="flex" alignItems="center">
|
||||||
<Heading size="md">{t('entities.title')}</Heading>
|
<Heading size="md">{t('entities.title')}</Heading>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
import { Button, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -8,83 +9,66 @@ import { Route } from 'models/Routes';
|
|||||||
|
|
||||||
const variantChange = '0.2s linear';
|
const variantChange = '0.2s linear';
|
||||||
|
|
||||||
interface Props {
|
const commonStyle = {
|
||||||
activeRoute: (path: string, otherRoute: string | undefined) => string;
|
boxSize: 'initial',
|
||||||
route: Route;
|
justifyContent: 'flex-start',
|
||||||
role: string;
|
alignItems: 'center',
|
||||||
}
|
transition: variantChange,
|
||||||
|
bg: 'transparent',
|
||||||
|
ps: '6px',
|
||||||
|
py: '12px',
|
||||||
|
pe: '4px',
|
||||||
|
w: '100%',
|
||||||
|
borderRadius: '15px',
|
||||||
|
_active: {
|
||||||
|
bg: 'inherit',
|
||||||
|
transform: 'none',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
_focus: {
|
||||||
|
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
const NavLinkButton = (
|
type Props = {
|
||||||
{
|
isActive: boolean;
|
||||||
activeRoute,
|
route: Route;
|
||||||
route,
|
toggleSidebar: () => void;
|
||||||
role
|
};
|
||||||
}: Props
|
|
||||||
) => {
|
export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||||
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
||||||
|
|
||||||
|
if (route.navButton) {
|
||||||
|
return route.navButton(isActive, toggleSidebar, route) as JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink to={route.path} key={uuid()}>
|
<NavLink to={route.path.replace(':id', '0')} key={uuid()} style={{ width: '100%' }}>
|
||||||
{activeRoute(route.path, undefined) === 'active' ? (
|
{isActive ? (
|
||||||
<Button
|
<Button {...commonStyle} boxShadow="none">
|
||||||
hidden={route.hidden || !route.authorized.includes(role)}
|
|
||||||
boxSize="initial"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
alignItems="center"
|
|
||||||
boxShadow="none"
|
|
||||||
bg="transparent"
|
|
||||||
transition={variantChange}
|
|
||||||
mb="12px"
|
|
||||||
mx="auto"
|
|
||||||
ps="10px"
|
|
||||||
py="12px"
|
|
||||||
borderRadius="15px"
|
|
||||||
w="100%"
|
|
||||||
_active={{
|
|
||||||
bg: 'inherit',
|
|
||||||
transform: 'none',
|
|
||||||
borderColor: 'transparent',
|
|
||||||
}}
|
|
||||||
_focus={{
|
|
||||||
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex>
|
<Flex>
|
||||||
<IconBox bg="blue.300" color="white" h="42px" w="42px" me="12px" transition={variantChange}>
|
<IconBox bg="blue.300" color="white" h="38px" w="38px" me="6px" transition={variantChange}>
|
||||||
{route.icon(true)}
|
{route.icon(true)}
|
||||||
</IconBox>
|
</IconBox>
|
||||||
<Text color={activeTextColor} my="auto" fontSize="lg">
|
<Text color={activeTextColor} my="auto" fontSize="md">
|
||||||
{t(route.name)}
|
{t(route.name)}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
hidden={route.hidden || !route.authorized.includes(role)}
|
{...commonStyle}
|
||||||
boxSize="initial"
|
ps="6px"
|
||||||
justifyContent="flex-start"
|
|
||||||
alignItems="center"
|
|
||||||
bg="transparent"
|
|
||||||
mb="12px"
|
|
||||||
mx="auto"
|
|
||||||
py="12px"
|
|
||||||
ps="10px"
|
|
||||||
borderRadius="15px"
|
|
||||||
w="100%"
|
|
||||||
_active={{
|
|
||||||
bg: 'inherit',
|
|
||||||
transform: 'none',
|
|
||||||
borderColor: 'transparent',
|
|
||||||
}}
|
|
||||||
_focus={{
|
_focus={{
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex>
|
<Flex>
|
||||||
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="12px" transition={variantChange}>
|
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="6px" transition={variantChange}>
|
||||||
{route.icon(false)}
|
{route.icon(false)}
|
||||||
</IconBox>
|
</IconBox>
|
||||||
<Text color={inactiveTextColor} my="auto" fontSize="sm">
|
<Text color={inactiveTextColor} my="auto" fontSize="sm">
|
||||||
@@ -96,5 +80,3 @@ const NavLinkButton = (
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(NavLinkButton);
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { LegacyRef, useRef } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -8,55 +8,81 @@ import {
|
|||||||
DrawerOverlay,
|
DrawerOverlay,
|
||||||
Flex,
|
Flex,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useColorMode,
|
|
||||||
Text,
|
Text,
|
||||||
Spacer,
|
Spacer,
|
||||||
useBreakpoint,
|
useBreakpoint,
|
||||||
|
VStack,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import createLinks from './CreateLinks';
|
import { v4 as uuid } from 'uuid';
|
||||||
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
import { NavLinkButton } from './NavLinkButton';
|
||||||
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { Route } from 'models/Routes';
|
import { Route } from 'models/Routes';
|
||||||
|
|
||||||
const variantChange = '0.2s linear';
|
const variantChange = '0.2s linear';
|
||||||
|
|
||||||
interface Props {
|
export type SidebarProps = {
|
||||||
routes: Route[];
|
routes: Route[];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
}
|
logo: React.ReactNode;
|
||||||
|
version: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
topNav?: (isRouteActive: (str: string, str2: string) => boolean, toggleSidebar: () => void) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, children }: SidebarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const mainPanel = useRef<unknown>();
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const navbarShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
const navbarShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
const activeRoute = (routeName: string, otherRoute: string | undefined) => {
|
const isRouteActive = (routeName: string, otherRoute?: string) => {
|
||||||
if (otherRoute)
|
if (otherRoute)
|
||||||
return location.pathname.split('/')[1] === routeName.split('/')[1] ||
|
return (
|
||||||
|
location.pathname.split('/')[1] === routeName.split('/')[1] ||
|
||||||
location.pathname.split('/')[1] === otherRoute.split('/')[1]
|
location.pathname.split('/')[1] === otherRoute.split('/')[1]
|
||||||
? 'active'
|
);
|
||||||
: '';
|
|
||||||
|
|
||||||
return location.pathname === routeName ? 'active' : '';
|
return location.pathname === routeName.replace(':id', '0');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||||
|
|
||||||
const brand = (
|
const brand = (
|
||||||
<Box pt="25px" mb="12px">
|
<Box pt="25px" mb="15px" px="12px">
|
||||||
<img src={colorMode === 'light' ? lightLogo : darkLogo} alt="OpenWifi" width="180px" height="100px" />
|
{logo}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sidebarContent = React.useMemo(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||||
|
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||||
|
{routes
|
||||||
|
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||||
|
.map((route) => (
|
||||||
|
<NavLinkButton key={uuid()} isActive={isRouteActive(route.path)} route={route} toggleSidebar={toggle} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
<Spacer />
|
||||||
|
<Box mb={2}>{children}</Box>
|
||||||
|
<Box>
|
||||||
|
<Text color="gray.400">
|
||||||
|
{t('footer.version')} {version}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[user?.userRole, location],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Drawer isOpen={breakpoint === 'base' && isOpen} onClose={toggle} placement="left">
|
<Drawer isOpen={isCompact && isOpen} onClose={toggle} placement="left">
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent
|
<DrawerContent
|
||||||
w="250px"
|
w="250px"
|
||||||
@@ -69,52 +95,33 @@ const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
|||||||
}}
|
}}
|
||||||
borderRadius="16px"
|
borderRadius="16px"
|
||||||
>
|
>
|
||||||
<DrawerCloseButton _focus={{ boxShadow: 'none' }} _hover={{ boxShadow: 'none' }} />
|
<DrawerCloseButton />
|
||||||
<DrawerBody maxW="250px" px="1rem">
|
<DrawerBody maxW="250px" px="1rem">
|
||||||
<Box maxW="100%" h="90vh">
|
<Box maxW="100%" h="90vh">
|
||||||
<Box>{brand}</Box>
|
{brand}
|
||||||
<Flex direction="column" mb="40px" h="calc(100vh - 200px)" alignItems="center">
|
<Flex direction="column" mb="40px" h="calc(100vh - 200px)" alignItems="center" overflowY="auto">
|
||||||
<Box overflowY="auto">{createLinks(routes, activeRoute, user?.userRole ?? '', toggle)}</Box>
|
{sidebarContent}
|
||||||
<Spacer />
|
|
||||||
<Box>
|
|
||||||
<Text color="gray.400">
|
|
||||||
{t('footer.version')} {__APP_VERSION__}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Box ref={mainPanel as LegacyRef<HTMLDivElement> | undefined}>
|
<Box>
|
||||||
<Box hidden={!isOpen} position="fixed">
|
<Box hidden={isCompact} position="fixed">
|
||||||
<Box
|
<Box
|
||||||
shadow={navbarShadow}
|
shadow={navbarShadow}
|
||||||
bg={useColorModeValue('white', 'gray.700')}
|
bg={useColorModeValue('white', 'gray.700')}
|
||||||
transition={variantChange}
|
transition={variantChange}
|
||||||
w="200px"
|
w="200px"
|
||||||
maxW="200px"
|
maxW="200px"
|
||||||
ms={{
|
|
||||||
sm: '16px',
|
|
||||||
}}
|
|
||||||
my={{
|
|
||||||
sm: '16px',
|
|
||||||
}}
|
|
||||||
h="calc(100vh - 32px)"
|
h="calc(100vh - 32px)"
|
||||||
ps="20px"
|
my="16px"
|
||||||
pe="20px"
|
ml="16px"
|
||||||
m="16px 0px 16px 16px"
|
|
||||||
borderRadius="16px"
|
borderRadius="16px"
|
||||||
>
|
>
|
||||||
<Box>{brand}</Box>
|
{brand}
|
||||||
<Flex direction="column" mb="40px" h="calc(100vh - 180px)" alignItems="center">
|
<Flex direction="column" h="calc(100vh - 160px)" alignItems="center" overflowY="auto">
|
||||||
<Box overflowY="auto">{createLinks(routes, activeRoute, user?.userRole ?? '', toggle)}</Box>
|
{sidebarContent}
|
||||||
<Spacer />
|
|
||||||
<Box>
|
|
||||||
<Text color="gray.400">
|
|
||||||
{t('footer.version')} {__APP_VERSION__}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -122,5 +129,3 @@ const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
|
||||||
|
|||||||
@@ -1,56 +1,66 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React from 'react';
|
||||||
import { Flex, Portal, Spinner, useBoolean, useBreakpoint } from '@chakra-ui/react';
|
import { useBoolean, useBreakpoint, useColorMode } from '@chakra-ui/react';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import PanelContainer from './Containers/PanelContainer';
|
import { Navbar } from './Navbar';
|
||||||
import PanelContent from './Containers/PanelContent';
|
import { PageContainer } from './PageContainer';
|
||||||
import MainPanel from './MainPanel';
|
import { Sidebar } from './Sidebar';
|
||||||
import Navbar from './Navbar';
|
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
||||||
import Sidebar from './Sidebar';
|
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
||||||
import CreateRootModal from 'components/Modals/Entity/CreateRootModal';
|
import LanguageSwitcher from 'components/LanguageSwitcher';
|
||||||
import { Route as RouteProps } from 'models/Routes';
|
import { Route as RouteType } from 'models/Routes';
|
||||||
import NotFoundPage from 'pages/NotFound';
|
import NotFoundPage from 'pages/NotFound';
|
||||||
import routes from 'router/routes';
|
import routes from 'router/routes';
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const location = useLocation();
|
||||||
const breakpoint = useBreakpoint('xl');
|
const breakpoint = useBreakpoint('xl');
|
||||||
const [isSidebarOpen, { toggle: toggleSidebar }] = useBoolean(breakpoint !== 'base' && breakpoint !== 'sm');
|
const [isSidebarOpen, { toggle: toggleSidebar }] = useBoolean(breakpoint !== 'base' && breakpoint !== 'sm');
|
||||||
document.documentElement.dir = 'ltr';
|
document.documentElement.dir = 'ltr';
|
||||||
|
|
||||||
const getRoutes = (r: RouteProps[]) =>
|
const activeRoute = React.useMemo(() => {
|
||||||
|
const route = routes.find(
|
||||||
|
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (route) return route.navName ? t(route.navName) : t(route.name);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}, [t, location.pathname]);
|
||||||
|
|
||||||
|
const getRoutes = (r: RouteType[]) =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
r.map((route: RouteProps) => <Route path={route.path} element={<route.component />} key={uuid()} />);
|
r.map((route: RouteType) => <Route path={route.path} element={<route.component />} key={uuid()} />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar routes={routes} isOpen={isSidebarOpen} toggle={toggleSidebar} />
|
<Sidebar
|
||||||
<Portal>
|
routes={routes}
|
||||||
<Navbar secondary={false} toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} />
|
isOpen={isSidebarOpen}
|
||||||
</Portal>
|
toggle={toggleSidebar}
|
||||||
<MainPanel
|
version={__APP_VERSION__}
|
||||||
w={{
|
logo={
|
||||||
base: '100%',
|
<img
|
||||||
sm: isSidebarOpen ? 'calc(100% - 220px)' : '100%',
|
src={colorMode === 'light' ? lightLogo : darkLogo}
|
||||||
md: isSidebarOpen ? 'calc(100% - 220px)' : '100%',
|
alt="OpenWifi"
|
||||||
}}
|
width="180px"
|
||||||
>
|
height="100px"
|
||||||
<CreateRootModal />
|
style={{
|
||||||
<PanelContent>
|
marginLeft: 'auto',
|
||||||
<PanelContainer>
|
marginRight: 'auto',
|
||||||
<Suspense
|
}}
|
||||||
fallback={
|
/>
|
||||||
<Flex flexDirection="column" pt="75px">
|
}
|
||||||
<Spinner />
|
/>
|
||||||
</Flex>
|
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
|
||||||
}
|
<PageContainer waitForUser>
|
||||||
>
|
<Routes>
|
||||||
<Routes>
|
{[...getRoutes(routes as RouteType[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
||||||
{[...getRoutes(routes as RouteProps[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
</Routes>
|
||||||
</Routes>
|
</PageContainer>
|
||||||
</Suspense>
|
|
||||||
</PanelContainer>
|
|
||||||
</PanelContent>
|
|
||||||
</MainPanel>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
238
src/models/Analytics.ts
Normal file
238
src/models/Analytics.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import { Note } from './Note';
|
||||||
|
|
||||||
|
export type AnalyticsBoardDevice = {
|
||||||
|
associations_2g: number;
|
||||||
|
associations_5g: number;
|
||||||
|
associations_6g: number;
|
||||||
|
boardId: string;
|
||||||
|
connected: boolean;
|
||||||
|
connectionIp: string;
|
||||||
|
deviceType: string;
|
||||||
|
health: number;
|
||||||
|
lastConnection: number;
|
||||||
|
lastContact: number;
|
||||||
|
lastDisconnection: number;
|
||||||
|
lastFirmware: string;
|
||||||
|
lastFirmwareUpdate: number;
|
||||||
|
lastHealth: number;
|
||||||
|
lastPing: number;
|
||||||
|
lastState: number;
|
||||||
|
locale: string;
|
||||||
|
memory: number;
|
||||||
|
pings: number;
|
||||||
|
serialNumber: string;
|
||||||
|
states: number;
|
||||||
|
type: string;
|
||||||
|
uptime: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsBoardDevicesApiResponse = {
|
||||||
|
devices: AnalyticsBoardDevice[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsBoardApiResponse = {
|
||||||
|
created: number;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modified: number;
|
||||||
|
name: string;
|
||||||
|
notes: Note[];
|
||||||
|
tags: string[];
|
||||||
|
venueList: {
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
interval: number;
|
||||||
|
monitorSubVenues: boolean;
|
||||||
|
name: string;
|
||||||
|
retention: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsClientLifecycleApiResponse = {
|
||||||
|
ack_signal: number;
|
||||||
|
ack_signal_avg: number;
|
||||||
|
active_ms: number;
|
||||||
|
bssid: string;
|
||||||
|
busy_ms: number;
|
||||||
|
channel: number;
|
||||||
|
channel_width: number;
|
||||||
|
connected: number;
|
||||||
|
inactive: number;
|
||||||
|
ipv4: string;
|
||||||
|
ipv6: string;
|
||||||
|
mode: string;
|
||||||
|
noise: number;
|
||||||
|
receive_ms: number;
|
||||||
|
rssi: number;
|
||||||
|
rx_bitrate: number;
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_chwidth: number;
|
||||||
|
rx_duration: number;
|
||||||
|
rx_mcs: number;
|
||||||
|
rx_nss: number;
|
||||||
|
rx_packets: number;
|
||||||
|
rx_vht: boolean;
|
||||||
|
ssid: string;
|
||||||
|
station_id: string;
|
||||||
|
timestamp: number;
|
||||||
|
tx_bitrate: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_chwidth: number;
|
||||||
|
tx_duration: number;
|
||||||
|
tx_mcs: number;
|
||||||
|
tx_nss: number;
|
||||||
|
tx_packets: number;
|
||||||
|
tx_power: number;
|
||||||
|
tx_retries: number;
|
||||||
|
tx_vht: boolean;
|
||||||
|
venue_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsApData = {
|
||||||
|
collisions: number;
|
||||||
|
multicast: number;
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_bytes_bw: number;
|
||||||
|
rx_bytes_delta: number;
|
||||||
|
rx_dropped: number;
|
||||||
|
rx_dropped_delta: number;
|
||||||
|
rx_dropped_pct: number;
|
||||||
|
rx_errors: number;
|
||||||
|
rx_errors_delta: number;
|
||||||
|
rx_errors_pct: number;
|
||||||
|
rx_packets: number;
|
||||||
|
rx_packets_bw: number;
|
||||||
|
rx_packets_delta: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_bytes_bw: number;
|
||||||
|
tx_bytes_delta: number;
|
||||||
|
tx_dropped: number;
|
||||||
|
tx_dropped_delta: number;
|
||||||
|
tx_dropped_pct: number;
|
||||||
|
tx_errors: number;
|
||||||
|
tx_errors_delta: number;
|
||||||
|
tx_errors_pct: number;
|
||||||
|
tx_packets: number;
|
||||||
|
tx_packets_bw: number;
|
||||||
|
tx_packets_delta: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsRadioData = {
|
||||||
|
active_ms: number;
|
||||||
|
active_pct: number;
|
||||||
|
band: number;
|
||||||
|
busy_ms: number;
|
||||||
|
busy_pct: number;
|
||||||
|
channel: number;
|
||||||
|
channel_width: number;
|
||||||
|
noise: number;
|
||||||
|
receive_ms: number;
|
||||||
|
receive_pct: number;
|
||||||
|
temperature: number;
|
||||||
|
transmit_ms: number;
|
||||||
|
transmit_pct: number;
|
||||||
|
tx_power: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsAssociationData = {
|
||||||
|
connected: number;
|
||||||
|
inactive: number;
|
||||||
|
rssi: number;
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_bytes_bw: number;
|
||||||
|
rx_bytes_delta: number;
|
||||||
|
rx_packets: number;
|
||||||
|
rx_packets_bw: number;
|
||||||
|
rx_packets_delta: number;
|
||||||
|
rx_rate: {
|
||||||
|
bitrate: number;
|
||||||
|
chwidth: number;
|
||||||
|
ht: boolean;
|
||||||
|
mcs: number;
|
||||||
|
nss: number;
|
||||||
|
sgi: boolean;
|
||||||
|
};
|
||||||
|
station: string;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_bytes_bw: number;
|
||||||
|
tx_bytes_delta: number;
|
||||||
|
tx_duration: number;
|
||||||
|
tx_duration_delta: number;
|
||||||
|
tx_duration_pct: number;
|
||||||
|
tx_failed: number;
|
||||||
|
tx_failed_delta: number;
|
||||||
|
tx_failed_pct: number;
|
||||||
|
tx_packets: number;
|
||||||
|
tx_packets_bw: number;
|
||||||
|
tx_packets_delta: number;
|
||||||
|
tx_rate: {
|
||||||
|
bitrate: number;
|
||||||
|
chwidth: number;
|
||||||
|
ht: boolean;
|
||||||
|
mcs: number;
|
||||||
|
nss: number;
|
||||||
|
sgi: boolean;
|
||||||
|
};
|
||||||
|
tx_retries: number;
|
||||||
|
tx_retries_delta: number;
|
||||||
|
tx_retries_pct: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsSsidData = {
|
||||||
|
associations: AnalyticsAssociationData[];
|
||||||
|
band: 2;
|
||||||
|
bssid: string;
|
||||||
|
channel: number;
|
||||||
|
mode: string;
|
||||||
|
rx_bytes_bw: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
rx_packets_bw: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
ssid: string;
|
||||||
|
tx_bytes_bw: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
tx_duration_pct: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
tx_failed_pct: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
tx_packets_bw: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
tx_retries_pct: {
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsTimePointApiResponse = {
|
||||||
|
ap_data: AnalyticsApData;
|
||||||
|
boardId: string;
|
||||||
|
device_info: AnalyticsBoardDevice;
|
||||||
|
id: string;
|
||||||
|
radio_data: AnalyticsRadioData[];
|
||||||
|
serialNumber: string;
|
||||||
|
ssid_data: AnalyticsSsidData[];
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyticsTimePointsApiResponse = {
|
||||||
|
points: AnalyticsTimePointApiResponse[][];
|
||||||
|
};
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { DeviceRules } from './Basic';
|
||||||
|
import { Note } from './Note';
|
||||||
|
|
||||||
export interface Entity {
|
export interface Entity {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -6,4 +9,14 @@ export interface Entity {
|
|||||||
venues: string[];
|
venues: string[];
|
||||||
contacts: string[];
|
contacts: string[];
|
||||||
entity: string;
|
entity: string;
|
||||||
|
created: number;
|
||||||
|
modified: number;
|
||||||
|
description: string;
|
||||||
|
deviceRules: DeviceRules;
|
||||||
|
sourceIP: string[];
|
||||||
|
notes: Note[];
|
||||||
|
children: string[];
|
||||||
|
configurations: string[];
|
||||||
|
locations: string[];
|
||||||
|
variables: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface Route {
|
export type Route = {
|
||||||
authorized: string[];
|
authorized: string[];
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
navName?: string;
|
navName?: string;
|
||||||
icon: (active: boolean) => ReactNode;
|
icon: (active: boolean) => ReactNode;
|
||||||
|
navButton?: (isActive: boolean, toggleSidebar: () => void, route: Route) => React.ReactNode;
|
||||||
isEntity?: boolean;
|
isEntity?: boolean;
|
||||||
component: unknown;
|
component: unknown;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
export interface Venue {
|
import { DeviceRules } from './Basic';
|
||||||
|
import { Note } from './Note';
|
||||||
|
|
||||||
|
export interface VenueApiResponse {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description: string;
|
||||||
parent: string;
|
parent: string;
|
||||||
devices: string[];
|
devices: string[];
|
||||||
venues: string[];
|
children: string[];
|
||||||
contacts: string[];
|
contacts: string[];
|
||||||
entity: string;
|
entity: string;
|
||||||
|
boards: string[];
|
||||||
|
created: number;
|
||||||
|
modified: number;
|
||||||
|
configurations: string[];
|
||||||
|
notes: Note[];
|
||||||
|
variables: string[];
|
||||||
|
location: string;
|
||||||
|
sourceIP: string[];
|
||||||
|
deviceRules: DeviceRules;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import ConfigurationCard from './ConfigurationCard';
|
import ConfigurationCard from './ConfigurationCard';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const ConfigurationPage = () => {
|
const ConfigurationPage = () => {
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
return (
|
return id !== '' ? <ConfigurationCard id={id} /> : null;
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
{isUserLoaded && id !== '' && <ConfigurationCard id={id} />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConfigurationPage;
|
export default ConfigurationPage;
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ const CreateEntityForm = ({ isOpen, onClose, formRef, parentId }) => {
|
|||||||
validationSchema={EntitySchema(t)}
|
validationSchema={EntitySchema(t)}
|
||||||
onSubmit={(formData, { setSubmitting, resetForm }) =>
|
onSubmit={(formData, { setSubmitting, resetForm }) =>
|
||||||
create.mutateAsync(createParameters(formData), {
|
create.mutateAsync(createParameters(formData), {
|
||||||
onSuccess: ({ data }) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries(['get-entity-tree']);
|
queryClient.invalidateQueries(['get-entity-tree']);
|
||||||
|
queryClient.invalidateQueries(['get-entity', parentId]);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import CreateEntityForm from './Form';
|
import CreateEntityForm from './Form';
|
||||||
import CloseButton from 'components/Buttons/CloseButton';
|
import CloseButton from 'components/Buttons/CloseButton';
|
||||||
import CreateButton from 'components/Buttons/CreateButton';
|
|
||||||
import SaveButton from 'components/Buttons/SaveButton';
|
import SaveButton from 'components/Buttons/SaveButton';
|
||||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||||
import ModalHeader from 'components/Modals/ModalHeader';
|
import ModalHeader from 'components/Modals/ModalHeader';
|
||||||
@@ -12,16 +11,16 @@ import useFormRef from 'hooks/useFormRef';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
parentId: PropTypes.string.isRequired,
|
parentId: PropTypes.string.isRequired,
|
||||||
isDisabled: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
isDisabled: false,
|
isOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateEntityModal = ({ parentId, isDisabled }) => {
|
const CreateEntityModal = ({ parentId, onClose, isOpen }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||||
const { form, formRef } = useFormRef();
|
const { form, formRef } = useFormRef();
|
||||||
|
|
||||||
@@ -33,31 +32,28 @@ const CreateEntityModal = ({ parentId, isDisabled }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
||||||
<CreateButton onClick={onOpen} isDisabled={isDisabled} />
|
<ModalOverlay />
|
||||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl">
|
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||||
<ModalOverlay />
|
<ModalHeader
|
||||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
title={t('crud.create_object', { obj: t('entities.sub_one') })}
|
||||||
<ModalHeader
|
right={
|
||||||
title={t('crud.create_object', { obj: t('entities.sub_one') })}
|
<>
|
||||||
right={
|
<SaveButton
|
||||||
<>
|
onClick={form.submitForm}
|
||||||
<SaveButton
|
isLoading={form.isSubmitting}
|
||||||
onClick={form.submitForm}
|
isDisabled={!form.isValid || !form.dirty}
|
||||||
isLoading={form.isSubmitting}
|
/>
|
||||||
isDisabled={!form.isValid || !form.dirty}
|
<CloseButton ml={2} onClick={closeModal} />
|
||||||
/>
|
</>
|
||||||
<CloseButton ml={2} onClick={closeModal} />
|
}
|
||||||
</>
|
/>
|
||||||
}
|
<ModalBody>
|
||||||
/>
|
<CreateEntityForm isOpen={isOpen} onClose={onClose} formRef={formRef} parentId={parentId} />
|
||||||
<ModalBody>
|
</ModalBody>
|
||||||
<CreateEntityForm isOpen={isOpen} onClose={onClose} formRef={formRef} parentId={parentId} />
|
</ModalContent>
|
||||||
</ModalBody>
|
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||||
</ModalContent>
|
</Modal>
|
||||||
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,24 +14,19 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
import { useDeleteEntity } from 'hooks/Network/Entity';
|
import { useDeleteEntity } from 'hooks/Network/Entity';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
import { Entity } from 'models/Entity';
|
||||||
|
|
||||||
const propTypes = {
|
type Props = {
|
||||||
entity: PropTypes.shape(EntityShape),
|
entity?: Entity;
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const DeleteEntityPopover = ({ entity, isDisabled }: Props) => {
|
||||||
entity: { name: '', id: '' },
|
|
||||||
isDisabled: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -40,7 +35,7 @@ const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|||||||
const deleteEntity = useDeleteEntity();
|
const deleteEntity = useDeleteEntity();
|
||||||
|
|
||||||
const handleDeleteClick = () =>
|
const handleDeleteClick = () =>
|
||||||
deleteEntity.mutateAsync(entity.id, {
|
deleteEntity.mutateAsync(entity?.id ?? '', {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(['get-entity-tree']);
|
queryClient.invalidateQueries(['get-entity-tree']);
|
||||||
onClose();
|
onClose();
|
||||||
@@ -48,14 +43,14 @@ const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|||||||
id: `entity-delete-success`,
|
id: `entity-delete-success`,
|
||||||
title: t('common.success'),
|
title: t('common.success'),
|
||||||
description: t('crud.success_delete_obj', {
|
description: t('crud.success_delete_obj', {
|
||||||
obj: entity.name,
|
obj: entity?.name,
|
||||||
}),
|
}),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
});
|
});
|
||||||
navigate(`/entity/${entity.parent}`);
|
navigate(`/entity/${entity?.parent}`);
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
if (!toast.isActive('entity-fetching-error'))
|
if (!toast.isActive('entity-fetching-error'))
|
||||||
@@ -63,8 +58,8 @@ const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|||||||
id: 'entity-delete-error',
|
id: 'entity-delete-error',
|
||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
description: t('crud.error_delete_obj', {
|
description: t('crud.error_delete_obj', {
|
||||||
obj: entity.name,
|
obj: entity?.name,
|
||||||
e: e?.response?.data?.ErrorDescription,
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
}),
|
}),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
@@ -74,12 +69,12 @@ const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entity.children?.length > 0 || entity.venues?.length > 0) {
|
if (entity && (entity.children?.length > 0 || entity.venues?.length > 0)) {
|
||||||
return (
|
return (
|
||||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<span>
|
<span>
|
||||||
<DeleteButton onClick={onOpen} isDisabled={isDisabled} ml={2} />
|
<DeleteButton onClick={onOpen} isDisabled={isDisabled} isCompact />
|
||||||
</span>
|
</span>
|
||||||
</PopoverAnchor>
|
</PopoverAnchor>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
@@ -103,7 +98,7 @@ const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|||||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<span>
|
<span>
|
||||||
<DeleteButton onClick={onOpen} isDisabled={isDisabled} ml={2} />
|
<DeleteButton onClick={onOpen} isDisabled={isDisabled} isCompact />
|
||||||
</span>
|
</span>
|
||||||
</PopoverAnchor>
|
</PopoverAnchor>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
@@ -126,6 +121,4 @@ const DeleteEntityPopover = ({ entity, isDisabled }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteEntityPopover.propTypes = propTypes;
|
|
||||||
DeleteEntityPopover.defaultProps = defaultProps;
|
|
||||||
export default DeleteEntityPopover;
|
export default DeleteEntityPopover;
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useToast, Tabs, TabList, TabPanels, TabPanel, Tab, SimpleGrid, Box } from '@chakra-ui/react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { Formik, Field, Form } from 'formik';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import DeviceRulesField from 'components/CustomFields/DeviceRulesField';
|
|
||||||
import IpDetectionModalField from 'components/CustomFields/IpDetectionModalField';
|
|
||||||
import NotesTable from 'components/CustomFields/NotesTable';
|
|
||||||
import FormattedDate from 'components/FormattedDate';
|
|
||||||
import StringField from 'components/FormFields/StringField';
|
|
||||||
import { EntitySchema } from 'constants/formSchemas';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
import { useUpdateEntity } from 'hooks/Network/Entity';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
editing: PropTypes.bool.isRequired,
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
formRef: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
stopEditing: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditEntityForm = ({ editing, entity, formRef, stopEditing }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const toast = useToast();
|
|
||||||
const [formKey, setFormKey] = useState(uuid());
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const updateEntity = useUpdateEntity({ id: entity.id });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFormKey(uuid());
|
|
||||||
}, [editing]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
innerRef={formRef}
|
|
||||||
enableReinitialize
|
|
||||||
key={formKey}
|
|
||||||
initialValues={{ ...entity, rrm: entity.rrm !== '' ? entity.rrm : 'inherit' }}
|
|
||||||
validationSchema={EntitySchema(t)}
|
|
||||||
onSubmit={({ name, description, sourceIP, notes, deviceRules }, { setSubmitting, resetForm }) =>
|
|
||||||
updateEntity.mutateAsync(
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
deviceRules,
|
|
||||||
sourceIP,
|
|
||||||
notes: notes.filter((note) => note.isNew),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: ({ data }) => {
|
|
||||||
setSubmitting(false);
|
|
||||||
toast({
|
|
||||||
id: 'entity-update-success',
|
|
||||||
title: t('common.success'),
|
|
||||||
description: t('crud.success_update_obj', {
|
|
||||||
obj: t('entities.one'),
|
|
||||||
}),
|
|
||||||
status: 'success',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
queryClient.setQueryData(['get-entity', entity.id], data);
|
|
||||||
queryClient.invalidateQueries(['get-entity-tree']);
|
|
||||||
resetForm();
|
|
||||||
stopEditing();
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
toast({
|
|
||||||
id: uuid(),
|
|
||||||
title: t('common.error'),
|
|
||||||
description: t('crud.error_update_obj', {
|
|
||||||
obj: t('entities.one'),
|
|
||||||
e: e?.response?.data?.ErrorDescription,
|
|
||||||
}),
|
|
||||||
status: 'error',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
setSubmitting(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ errors, touched, setFieldValue }) => (
|
|
||||||
<Tabs variant="enclosed" w="100%">
|
|
||||||
<TabList>
|
|
||||||
<Tab>{t('common.main')}</Tab>
|
|
||||||
<Tab>{t('common.notes')}</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanels>
|
|
||||||
<TabPanel>
|
|
||||||
<Form>
|
|
||||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
|
||||||
<StringField
|
|
||||||
name="name"
|
|
||||||
label={t('common.name')}
|
|
||||||
errors={errors}
|
|
||||||
touched={touched}
|
|
||||||
isDisabled={!editing}
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
<StringField
|
|
||||||
name="description"
|
|
||||||
label={t('common.description')}
|
|
||||||
errors={errors}
|
|
||||||
touched={touched}
|
|
||||||
isDisabled={!editing}
|
|
||||||
/>
|
|
||||||
<DeviceRulesField isDisabled={!editing} />
|
|
||||||
<IpDetectionModalField
|
|
||||||
name="sourceIP"
|
|
||||||
setFieldValue={setFieldValue}
|
|
||||||
errors={errors}
|
|
||||||
isDisabled={!editing}
|
|
||||||
/>
|
|
||||||
<StringField
|
|
||||||
name="created"
|
|
||||||
label={t('common.created')}
|
|
||||||
errors={errors}
|
|
||||||
touched={touched}
|
|
||||||
element={
|
|
||||||
<Box pl={1} pt={2}>
|
|
||||||
<FormattedDate date={entity.created} />
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StringField
|
|
||||||
name="modified"
|
|
||||||
label={t('common.modified')}
|
|
||||||
errors={errors}
|
|
||||||
touched={touched}
|
|
||||||
element={
|
|
||||||
<Box pl={1} pt={2}>
|
|
||||||
<FormattedDate date={entity.modified} />
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
</Form>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<Field name="notes">
|
|
||||||
{({ field }) => <NotesTable notes={field.value} setNotes={setFieldValue} isDisabled={!editing} />}
|
|
||||||
</Field>
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EditEntityForm.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default EditEntityForm;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Box, Center, Heading, Spacer, Spinner, useBoolean } from '@chakra-ui/react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import DeleteEntityPopover from './DeleteEntityPopover';
|
|
||||||
import EditEntityForm from './Form';
|
|
||||||
import RefreshButton from 'components/Buttons/RefreshButton';
|
|
||||||
import SaveButton from 'components/Buttons/SaveButton';
|
|
||||||
import ToggleEditButton from 'components/Buttons/ToggleEditButton';
|
|
||||||
import Card from 'components/Card';
|
|
||||||
import CardBody from 'components/Card/CardBody';
|
|
||||||
import CardHeader from 'components/Card/CardHeader';
|
|
||||||
import LoadingOverlay from 'components/LoadingOverlay';
|
|
||||||
import { useGetEntity } from 'hooks/Network/Entity';
|
|
||||||
import useFormRef from 'hooks/useFormRef';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityCard = ({ id }) => {
|
|
||||||
const [editing, setEditing] = useBoolean();
|
|
||||||
const { data: entity, refetch, isFetching } = useGetEntity({ id });
|
|
||||||
const { form, formRef } = useFormRef();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card mb={4}>
|
|
||||||
<CardHeader mb="10px" display="flex">
|
|
||||||
<Box pt={1}>
|
|
||||||
<Heading size="md">{entity?.name}</Heading>
|
|
||||||
</Box>
|
|
||||||
<Spacer />
|
|
||||||
<Box>
|
|
||||||
<SaveButton
|
|
||||||
onClick={form.submitForm}
|
|
||||||
isLoading={form.isSubmitting}
|
|
||||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
|
||||||
ml={2}
|
|
||||||
/>
|
|
||||||
<ToggleEditButton
|
|
||||||
toggleEdit={setEditing.toggle}
|
|
||||||
isEditing={editing}
|
|
||||||
isDisabled={isFetching}
|
|
||||||
isDirty={formRef.dirty}
|
|
||||||
ml={2}
|
|
||||||
/>
|
|
||||||
<DeleteEntityPopover isDisabled={editing || isFetching} entity={entity} />
|
|
||||||
<RefreshButton onClick={refetch} isFetching={isFetching} isDisabled={editing} ml={2} />
|
|
||||||
</Box>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
{!entity && isFetching ? (
|
|
||||||
<Center w="100%">
|
|
||||||
<Spinner size="xl" />
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<LoadingOverlay isLoading={isFetching}>
|
|
||||||
<EditEntityForm editing={editing} entity={entity} stopEditing={setEditing.off} formRef={formRef} />
|
|
||||||
</LoadingOverlay>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityCard.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default React.memo(EntityCard);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { MagnifyingGlass } from 'phosphor-react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
cell: PropTypes.shape({
|
|
||||||
original: PropTypes.shape({
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Actions = ({ cell: { original: entity } }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleGoToPage = () => navigate(`/entity/${entity.id}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex>
|
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
|
||||||
<IconButton ml={2} colorScheme="blue" icon={<MagnifyingGlass size={20} />} size="sm" onClick={handleGoToPage} />
|
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Actions.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default Actions;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Actions from './Actions';
|
|
||||||
import EntityTable from 'components/Tables/EntityTable';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
import CreateEntityModal from 'pages/EntityPage/CreateEntityModal';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityChildrenTableWrapper = ({ entity }) => {
|
|
||||||
const actions = useCallback((cell) => <Actions key={uuid()} cell={cell.row} />, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box textAlign="right" mb={2}>
|
|
||||||
<CreateEntityModal parentId={entity?.id ?? ''} />
|
|
||||||
</Box>
|
|
||||||
<EntityTable select={entity.children} actions={actions} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
EntityChildrenTableWrapper.propTypes = propTypes;
|
|
||||||
export default EntityChildrenTableWrapper;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { MagnifyingGlass } from 'phosphor-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import DeleteConfigurationButton from 'components/Tables/ConfigurationTable/DeleteConfigurationButton';
|
|
||||||
import { Configuration } from 'models/Configuration';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
cell: {
|
|
||||||
original: Configuration;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Actions = ({ cell: { original: configuration } }: Props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleGoToPage = () => navigate(`/configuration/${configuration.id}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex>
|
|
||||||
<DeleteConfigurationButton configuration={configuration} />
|
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('venues.go_to_page')}
|
|
||||||
ml={2}
|
|
||||||
colorScheme="blue"
|
|
||||||
icon={<MagnifyingGlass size={20} />}
|
|
||||||
size="sm"
|
|
||||||
onClick={handleGoToPage}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Actions;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Actions from './Actions';
|
|
||||||
import ConfigurationInUseModal from 'components/Modals/Configuration/ConfigurationInUseModal';
|
|
||||||
import ConfigurationsTable from 'components/Tables/ConfigurationTable';
|
|
||||||
import ConfigurationViewAffectedModal from 'components/Tables/ConfigurationTable/ConfigurationViewAffectedModal';
|
|
||||||
import CreateConfigurationModal from 'components/Tables/ConfigurationTable/CreateConfigurationModal';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityConfigurationsTableWrapper = ({ entity }) => {
|
|
||||||
const [config, setConfig] = useState(null);
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { isOpen: isInUseOpen, onOpen: openInUse, onClose: closeInUse } = useDisclosure();
|
|
||||||
const { isOpen: isAffectedOpen, onOpen: openAffected, onClose: closeAffected } = useDisclosure();
|
|
||||||
const openInUseModal = (newConf) => {
|
|
||||||
setConfig(newConf);
|
|
||||||
openInUse();
|
|
||||||
};
|
|
||||||
const openAffectedModal = (newConf) => {
|
|
||||||
setConfig(newConf);
|
|
||||||
openAffected();
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = useCallback(
|
|
||||||
(cell) => (
|
|
||||||
<Actions key={uuid()} cell={cell.row} openInUseModal={openInUseModal} openAffectedModal={openAffectedModal} />
|
|
||||||
),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const refresh = () => queryClient.invalidateQueries(['get-entity', entity.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box textAlign="right">
|
|
||||||
<CreateConfigurationModal entityId={`entity:${entity.id}`} refresh={refresh} />
|
|
||||||
</Box>
|
|
||||||
<ConfigurationsTable select={entity.configurations} actions={actions} />
|
|
||||||
<ConfigurationInUseModal isOpen={isInUseOpen} onClose={closeInUse} config={config} />
|
|
||||||
<ConfigurationViewAffectedModal isOpen={isAffectedOpen} onClose={closeAffected} config={config} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
EntityConfigurationsTableWrapper.propTypes = propTypes;
|
|
||||||
export default EntityConfigurationsTableWrapper;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Actions from './Actions';
|
|
||||||
import ContactTable from 'components/Tables/ContactTable';
|
|
||||||
import CreateContactModal from 'components/Tables/ContactTable/CreateContactModal';
|
|
||||||
import EditContactModal from 'components/Tables/ContactTable/EditContactModal';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityContactTableWrapper = ({ entity }) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [contact, setContact] = useState(null);
|
|
||||||
const [refreshId, setRefreshId] = useState(0);
|
|
||||||
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
|
||||||
|
|
||||||
const openEditModal = (newContact) => {
|
|
||||||
setContact(newContact);
|
|
||||||
openEdit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshEntity = () => queryClient.invalidateQueries(['get-entity', entity.id]);
|
|
||||||
|
|
||||||
const refetchLocations = () => {
|
|
||||||
setRefreshId(refreshId + 1);
|
|
||||||
refreshEntity();
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = useCallback(
|
|
||||||
(cell) => <Actions key={uuid()} cell={cell.row} refreshEntity={refreshEntity} openEditModal={openEditModal} />,
|
|
||||||
[refreshId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box textAlign="right" mb={2}>
|
|
||||||
<CreateContactModal refresh={refreshEntity} entityId={entity.id} />
|
|
||||||
</Box>
|
|
||||||
<ContactTable
|
|
||||||
select={entity.contacts}
|
|
||||||
actions={actions}
|
|
||||||
refreshId={refreshId}
|
|
||||||
ignoredColumns={['entity']}
|
|
||||||
openDetailsModal={openEditModal}
|
|
||||||
/>
|
|
||||||
<EditContactModal isOpen={isEditOpen} onClose={closeEdit} contact={contact} refresh={refetchLocations} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityContactTableWrapper.propTypes = propTypes;
|
|
||||||
export default EntityContactTableWrapper;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Actions from './Actions';
|
|
||||||
import LocationTable from 'components/Tables/LocationTable';
|
|
||||||
import CreateLocationModal from 'components/Tables/LocationTable/CreateLocationModal';
|
|
||||||
import EditLocationModal from 'components/Tables/LocationTable/EditLocationModal';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityLocationTableWrapper = ({ entity }) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [location, setLocation] = useState(null);
|
|
||||||
const [refreshId, setRefreshId] = useState(0);
|
|
||||||
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
|
||||||
|
|
||||||
const openEditModal = (newLoc) => {
|
|
||||||
setLocation(newLoc);
|
|
||||||
openEdit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshEntity = () => queryClient.invalidateQueries(['get-entity', entity.id]);
|
|
||||||
|
|
||||||
const refetchLocations = () => {
|
|
||||||
setRefreshId(refreshId + 1);
|
|
||||||
refreshEntity();
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = useCallback(
|
|
||||||
(cell) => <Actions key={uuid()} cell={cell.row} refreshEntity={refreshEntity} openEditModal={openEditModal} />,
|
|
||||||
[refreshId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box textAlign="right" mb={2}>
|
|
||||||
<CreateLocationModal refresh={refreshEntity} entityId={entity.id} />
|
|
||||||
</Box>
|
|
||||||
<LocationTable
|
|
||||||
select={entity.locations}
|
|
||||||
actions={actions}
|
|
||||||
refreshId={refreshId}
|
|
||||||
ignoredColumns={['entity']}
|
|
||||||
openDetailsModal={openEditModal}
|
|
||||||
/>
|
|
||||||
<EditLocationModal isOpen={isEditOpen} onClose={closeEdit} location={location} refresh={refetchLocations} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityLocationTableWrapper.propTypes = propTypes;
|
|
||||||
export default EntityLocationTableWrapper;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
|
||||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Actions from './Actions';
|
|
||||||
import CreateResourceModal from 'components/Modals/Resources/CreateModal';
|
|
||||||
import EditResourceModal from 'components/Modals/Resources/EditModal';
|
|
||||||
import ResourceTable from 'components/Tables/ResourceTable';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityResourcesTableWrapper = ({ entity }) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [resource, setResource] = useState(null);
|
|
||||||
const [refreshId, setRefreshId] = useState(0);
|
|
||||||
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
|
||||||
|
|
||||||
const openEditModal = (newResource) => {
|
|
||||||
setResource(newResource);
|
|
||||||
openEdit();
|
|
||||||
};
|
|
||||||
const openDetailsModalFromTable = (openedResource) => {
|
|
||||||
setResource(openedResource);
|
|
||||||
openEdit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshEntity = () => queryClient.invalidateQueries(['get-entity', entity.id]);
|
|
||||||
|
|
||||||
const refreshTable = () => {
|
|
||||||
setRefreshId(refreshId + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = useCallback(
|
|
||||||
(cell) => <Actions key={uuid()} cell={cell.row} refreshTable={refreshEntity} openEditModal={openEditModal} />,
|
|
||||||
[refreshId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box textAlign="right" mb={2}>
|
|
||||||
<CreateResourceModal refresh={refreshEntity} entityId={entity.id} />
|
|
||||||
</Box>
|
|
||||||
<ResourceTable
|
|
||||||
select={entity.variables}
|
|
||||||
actions={actions}
|
|
||||||
refreshId={refreshId}
|
|
||||||
ignoredColumns={['entity']}
|
|
||||||
openDetailsModal={openDetailsModalFromTable}
|
|
||||||
/>
|
|
||||||
<EditResourceModal isOpen={isEditOpen} onClose={closeEdit} resource={resource} refresh={refreshTable} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EntityResourcesTableWrapper.propTypes = propTypes;
|
|
||||||
export default EntityResourcesTableWrapper;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { MagnifyingGlass } from 'phosphor-react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
cell: PropTypes.shape({
|
|
||||||
original: PropTypes.shape({
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Actions = ({ cell: { original: venue } }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleGoToPage = () => navigate(`/venue/${venue.id}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex>
|
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
|
||||||
<IconButton ml={2} colorScheme="blue" icon={<MagnifyingGlass size={20} />} size="sm" onClick={handleGoToPage} />
|
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Actions.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default Actions;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import { Alert, Box, Center, Heading } from '@chakra-ui/react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Actions from './Actions';
|
|
||||||
import VenueTable from 'components/Tables/VenueTable';
|
|
||||||
import CreateVenueModal from 'components/Tables/VenueTable/CreateVenueModal';
|
|
||||||
import { EntityShape } from 'constants/propShapes';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
entity: PropTypes.shape(EntityShape).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityVenueTableWrapper = ({ entity }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const actions = useCallback((cell) => <Actions key={uuid()} cell={cell.row} />, []);
|
|
||||||
|
|
||||||
if (entity?.id === '0000-0000-0000') {
|
|
||||||
return (
|
|
||||||
<Center minHeight="334px">
|
|
||||||
<Alert colorScheme="red" size="xl">
|
|
||||||
<Heading size="md">{t('entities.venues_under_root')}</Heading>
|
|
||||||
</Alert>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box textAlign="right" mb={2}>
|
|
||||||
<CreateVenueModal entityId={entity.id} />
|
|
||||||
</Box>
|
|
||||||
<VenueTable select={entity.venues} actions={actions} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
EntityVenueTableWrapper.propTypes = propTypes;
|
|
||||||
export default EntityVenueTableWrapper;
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Alert, Center, Heading, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import EntityChildrenTableWrapper from './EntityChildrenTableWrapper';
|
|
||||||
import EntityConfigurationsTableWrapper from './EntityConfigurationsTableWrapper';
|
|
||||||
import EntityContactTableWrapper from './EntityContactTableWrapper ';
|
|
||||||
import EntityDeviceTableWrapper from './EntityDeviceTableWrapper';
|
|
||||||
import EntityLocationTableWrapper from './EntityLocationTableWrapper ';
|
|
||||||
import EntityResourcesTableWrapper from './EntityResourcesTableWrapper';
|
|
||||||
import EntityVenueTableWrapper from './EntityVenueTableWrapper';
|
|
||||||
import Card from 'components/Card';
|
|
||||||
import CardBody from 'components/Card/CardBody';
|
|
||||||
import LoadingOverlay from 'components/LoadingOverlay';
|
|
||||||
import { useGetEntity } from 'hooks/Network/Entity';
|
|
||||||
|
|
||||||
const getDefaultIndex = (id: string) => {
|
|
||||||
localStorage.getItem(`entity.${id}.lastActiveIndex`);
|
|
||||||
const index = parseInt(localStorage.getItem(`entity.${id}.lastActiveIndex`) || '0', 10);
|
|
||||||
|
|
||||||
if (index >= 0 && index <= 6) return index;
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EntityChildrenCard = ({ id }: { id: string }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { data: entity, isFetching } = useGetEntity({ id });
|
|
||||||
const [tabIndex, setTabIndex] = React.useState(getDefaultIndex(id));
|
|
||||||
|
|
||||||
const onTabChange = (index: number) => {
|
|
||||||
setTabIndex(index);
|
|
||||||
localStorage.setItem(`entity.${id}.lastActiveIndex`, index.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardBody>
|
|
||||||
<Tabs isLazy variant="enclosed" w="100%" index={tabIndex} onChange={onTabChange}>
|
|
||||||
<TabList>
|
|
||||||
<Tab>{t('entities.sub_other')}</Tab>
|
|
||||||
<Tab>{t('venues.sub_other')}</Tab>
|
|
||||||
<Tab>{t('configurations.title')}</Tab>
|
|
||||||
<Tab>{t('inventory.title')}</Tab>
|
|
||||||
<Tab>{t('locations.other')}</Tab>
|
|
||||||
<Tab>{t('contacts.other')}</Tab>
|
|
||||||
<Tab>{t('resources.title')}</Tab>
|
|
||||||
</TabList>
|
|
||||||
{!entity || isFetching ? (
|
|
||||||
<Center w="100%">
|
|
||||||
<Spinner size="xl" />
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<LoadingOverlay isLoading={isFetching}>
|
|
||||||
<TabPanels>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
<EntityChildrenTableWrapper entity={entity} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
<EntityVenueTableWrapper entity={entity} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
<EntityConfigurationsTableWrapper entity={entity} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
{id === '0000-0000-0000' ? (
|
|
||||||
<Center minHeight="334px">
|
|
||||||
<Alert colorScheme="red" size="xl">
|
|
||||||
<Heading size="md">{t('entities.devices_under_root')}</Heading>
|
|
||||||
</Alert>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<EntityDeviceTableWrapper entity={entity} />
|
|
||||||
)}
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
<EntityLocationTableWrapper entity={entity} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
<EntityContactTableWrapper entity={entity} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel overflowX="auto">
|
|
||||||
<EntityResourcesTableWrapper entity={entity} />
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</LoadingOverlay>
|
|
||||||
)}
|
|
||||||
</Tabs>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EntityChildrenCard;
|
|
||||||
77
src/pages/EntityPage/EntityDropdown.tsx
Normal file
77
src/pages/EntityPage/EntityDropdown.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuDivider,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Tooltip,
|
||||||
|
useBreakpoint,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { TreeStructure } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import CreateEntityModal from './CreateEntityModal';
|
||||||
|
import { useGetEntity, useGetSelectEntities } from 'hooks/Network/Entity';
|
||||||
|
import { Entity } from 'models/Entity';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityDropdown = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const getChildren = useGetSelectEntities({ select: getEntity.data?.children ?? [] });
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
const goToEntity = (entityId: string) => () => navigate(`/entity/${entityId}`);
|
||||||
|
|
||||||
|
const isCompact = breakpoint === 'base' || breakpoint === 'sm';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu>
|
||||||
|
<Tooltip label={`${t('entities.sub_other')} (${getEntity.data?.children.length ?? 0})`}>
|
||||||
|
{isCompact ? (
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
icon={<TreeStructure size={20} />}
|
||||||
|
aria-label={`${t('entities.sub_other')} (${getEntity.data?.children.length ?? 0})`}
|
||||||
|
colorScheme="pink"
|
||||||
|
isDisabled={!getEntity.data}
|
||||||
|
mx={2}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
aria-label={`${t('entities.sub_other')} (${getEntity.data?.children.length ?? 0})`}
|
||||||
|
colorScheme="pink"
|
||||||
|
isDisabled={!getEntity.data}
|
||||||
|
mx={2}
|
||||||
|
>{`${t('entities.sub_other')} (${getEntity.data?.children.length ?? 0})`}</MenuButton>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={onOpen}>{t('common.create')}</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
{getChildren.data
|
||||||
|
?.sort((a: Entity, b: Entity) => a.name.localeCompare(b.name))
|
||||||
|
.map(({ id: entityId, name }: Entity) => (
|
||||||
|
<MenuItem key={entityId} onClick={goToEntity(entityId)}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
)) ?? []}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
<CreateEntityModal isOpen={isOpen} onClose={onClose} parentId={getEntity.data?.id ?? ''} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityDropdown;
|
||||||
40
src/pages/EntityPage/EntityHeader.tsx
Normal file
40
src/pages/EntityPage/EntityHeader.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { HStack, Heading, Icon, Spacer } from '@chakra-ui/react';
|
||||||
|
import { TreeStructure } from 'phosphor-react';
|
||||||
|
import DeleteEntityPopover from './DeleteEntityPopover';
|
||||||
|
import EntityDropdown from './EntityDropdown';
|
||||||
|
import VenueDropdown from './VenueDropdown';
|
||||||
|
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityPageHeader = ({ id }: Props) => {
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card mb={4} p={2}>
|
||||||
|
<CardHeader display="flex">
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Icon my="auto" as={TreeStructure} color="inherit" boxSize="24px" mr={2} />
|
||||||
|
<Heading my="auto" size="md">
|
||||||
|
{getEntity.data?.name}
|
||||||
|
</Heading>
|
||||||
|
<EntityDropdown id={id} />
|
||||||
|
<VenueDropdown id={id} />
|
||||||
|
</HStack>
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<DeleteEntityPopover entity={getEntity.data} isDisabled={getEntity.isFetching || !getEntity.data} />
|
||||||
|
<RefreshButton onClick={getEntity.refetch} isFetching={getEntity.isFetching} isCompact />
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityPageHeader;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, HStack, IconButton, Spacer, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { MagnifyingGlass } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import CardBody from 'components/Card/CardBody';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import ConfigurationsTable from 'components/Tables/ConfigurationTable';
|
||||||
|
import CreateConfigurationModal from 'components/Tables/ConfigurationTable/CreateConfigurationModal';
|
||||||
|
import DeleteConfigurationButton from 'components/Tables/ConfigurationTable/DeleteConfigurationButton';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
import { Configuration } from 'models/Configuration';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityConfigurations = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleGoToPage = (configId: string) => () => navigate(`/configuration/${configId}`);
|
||||||
|
|
||||||
|
const actions = React.useCallback(
|
||||||
|
(cell: { row: { original: Configuration } }) => (
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<DeleteConfigurationButton configuration={cell.row.original} />
|
||||||
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('common.view_details')}
|
||||||
|
colorScheme="blue"
|
||||||
|
icon={<MagnifyingGlass size={20} />}
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGoToPage(cell.row.original.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={2} pt={2}>
|
||||||
|
<Spacer />
|
||||||
|
<CreateConfigurationModal entityId={`entity:${id}`} refresh={getEntity.refetch} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box w="100%" overflowX="auto">
|
||||||
|
<ConfigurationsTable select={getEntity.data?.configurations ?? []} actions={actions} />
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityConfigurations;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Spacer, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import EntityResourceActions from './ResourceActions';
|
||||||
|
import CardBody from 'components/Card/CardBody';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import CreateResourceModal from 'components/Modals/Resources/CreateModal';
|
||||||
|
import EditResourceModal from 'components/Modals/Resources/EditModal';
|
||||||
|
import ResourcesTable from 'components/Tables/ResourceTable';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
import { Resource } from 'models/Resource';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityResources = ({ id }: Props) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [resource, setResource] = React.useState<Resource>();
|
||||||
|
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
|
||||||
|
const refreshTable = () => {
|
||||||
|
queryClient.invalidateQueries(['get-resources-with-select']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (openedResource: Resource) => () => {
|
||||||
|
setResource(openedResource);
|
||||||
|
openEdit();
|
||||||
|
};
|
||||||
|
const openDetailsModalFromTable = (openedResource: Resource) => {
|
||||||
|
setResource(openedResource);
|
||||||
|
openEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = React.useCallback(
|
||||||
|
(cell: { row: { original: Resource } }) => (
|
||||||
|
<EntityResourceActions
|
||||||
|
resource={cell.row.original}
|
||||||
|
refreshTable={getEntity.refetch}
|
||||||
|
openEditModal={openEditModal(cell.row.original)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={2} pt={2}>
|
||||||
|
<Spacer />
|
||||||
|
<CreateResourceModal refresh={getEntity.refetch} entityId={getEntity.data?.id ?? ''} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box w="100%" overflowX="auto">
|
||||||
|
<ResourcesTable
|
||||||
|
select={getEntity.data?.variables ?? []}
|
||||||
|
actions={actions}
|
||||||
|
openDetailsModal={openDetailsModalFromTable}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
<EditResourceModal isOpen={isEditOpen} onClose={closeEdit} resource={resource} refresh={refreshTable} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityResources;
|
||||||
@@ -18,23 +18,19 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useDeleteResource } from 'hooks/Network/Resources';
|
import { useDeleteResource } from 'hooks/Network/Resources';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
import { Resource } from 'models/Resource';
|
||||||
|
|
||||||
const propTypes = {
|
type Props = {
|
||||||
cell: PropTypes.shape({
|
resource: Resource;
|
||||||
original: PropTypes.shape({
|
refreshTable: () => void;
|
||||||
id: PropTypes.string.isRequired,
|
openEditModal: (resource: Resource) => void;
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
refreshTable: PropTypes.func.isRequired,
|
|
||||||
openEditModal: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Actions = ({ cell: { original: resource }, refreshTable, openEditModal }) => {
|
const EntityResourceActions = ({ resource, refreshTable, openEditModal }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
@@ -63,7 +59,7 @@ const Actions = ({ cell: { original: resource }, refreshTable, openEditModal })
|
|||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
description: t('crud.error_delete_obj', {
|
description: t('crud.error_delete_obj', {
|
||||||
obj: resource.name,
|
obj: resource.name,
|
||||||
e: e?.response?.data?.ErrorDescription,
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
}),
|
}),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
@@ -80,7 +76,7 @@ const Actions = ({ cell: { original: resource }, refreshTable, openEditModal })
|
|||||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||||
<Box>
|
<Box>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -105,6 +101,7 @@ const Actions = ({ cell: { original: resource }, refreshTable, openEditModal })
|
|||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label={t('common.view_details')}
|
||||||
ml={2}
|
ml={2}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
icon={<MagnifyingGlass size={20} />}
|
icon={<MagnifyingGlass size={20} />}
|
||||||
@@ -116,6 +113,4 @@ const Actions = ({ cell: { original: resource }, refreshTable, openEditModal })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Actions.propTypes = propTypes;
|
export default EntityResourceActions;
|
||||||
|
|
||||||
export default Actions;
|
|
||||||
56
src/pages/EntityPage/Layout/ConfigurationCard/index.tsx
Normal file
56
src/pages/EntityPage/Layout/ConfigurationCard/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import EntityConfigurations from './EntityConfigurations';
|
||||||
|
import EntityResources from './EntityResources';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigurationCard = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card p={0}>
|
||||||
|
<Tabs variant="enclosed" isLazy>
|
||||||
|
<TabList>
|
||||||
|
<CardHeader>
|
||||||
|
<Tab>{t('configurations.title')}</Tab>
|
||||||
|
<Tab>{t('resources.title')}</Tab>
|
||||||
|
</CardHeader>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel p={0}>
|
||||||
|
<Box
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderRight="1px solid"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
|
borderBottomLeftRadius="15px"
|
||||||
|
borderBottomRightRadius="15px"
|
||||||
|
>
|
||||||
|
<EntityConfigurations id={id} />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel p={0}>
|
||||||
|
<Box
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderRight="1px solid"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
|
borderBottomLeftRadius="15px"
|
||||||
|
borderBottomRightRadius="15px"
|
||||||
|
>
|
||||||
|
<EntityResources id={id} />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationCard;
|
||||||
38
src/pages/EntityPage/Layout/EntityChildren.tsx
Normal file
38
src/pages/EntityPage/Layout/EntityChildren.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Heading, Spacer } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import CreateEntityModal from '../CreateEntityModal';
|
||||||
|
import EntityChildrenActions from './EntityChildrenActions';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import EntityTable from 'components/Tables/EntityTable';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
import { Entity } from 'models/Entity';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityChildren = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const actions = React.useCallback(
|
||||||
|
(cell: { row: { original: Entity } }) => <EntityChildrenActions entity={cell.row.original} isVenue />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Heading size="md" my="auto">
|
||||||
|
{t('entities.sub_other')}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
<CreateEntityModal parentId={getEntity.data?.id ?? ''} />
|
||||||
|
</CardHeader>
|
||||||
|
<EntityTable select={getEntity.data?.children ?? []} actions={actions} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityChildren;
|
||||||
@@ -3,24 +3,21 @@ import { Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
|||||||
import { MagnifyingGlass } from 'phosphor-react';
|
import { MagnifyingGlass } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import DeleteConfigurationButton from 'components/Tables/ConfigurationTable/DeleteConfigurationButton';
|
import { Entity } from 'models/Entity';
|
||||||
import { Configuration } from 'models/Configuration';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cell: {
|
entity: Entity;
|
||||||
original: Configuration;
|
isVenue: boolean;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Actions = ({ cell: { original: configuration } }: Props) => {
|
const EntityChildrenActions = ({ entity, isVenue }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleGoToPage = () => navigate(`/configuration/${configuration.id}`);
|
const handleGoToPage = () => navigate(isVenue ? `/venue/${entity.id}` : `/entity/${entity.id}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex>
|
<Flex>
|
||||||
<DeleteConfigurationButton configuration={configuration} />
|
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('common.view_details')}
|
aria-label={t('common.view_details')}
|
||||||
@@ -35,4 +32,4 @@ const Actions = ({ cell: { original: configuration } }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Actions;
|
export default EntityChildrenActions;
|
||||||
161
src/pages/EntityPage/Layout/EntityDetails.tsx
Normal file
161
src/pages/EntityPage/Layout/EntityDetails.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
HStack,
|
||||||
|
Heading,
|
||||||
|
SimpleGrid,
|
||||||
|
Spacer,
|
||||||
|
Spinner,
|
||||||
|
useBoolean,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import SaveButton from 'components/Buttons/SaveButton';
|
||||||
|
import ToggleEditButton from 'components/Buttons/ToggleEditButton';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardBody from 'components/Card/CardBody';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import DeviceRulesField from 'components/CustomFields/DeviceRulesField';
|
||||||
|
import IpDetectionModalField from 'components/CustomFields/IpDetectionModalField';
|
||||||
|
import FormattedDate from 'components/FormattedDate';
|
||||||
|
import StringField from 'components/FormFields/StringField';
|
||||||
|
import { EntitySchema } from 'constants/formSchemas';
|
||||||
|
import { useGetEntity, useUpdateEntity } from 'hooks/Network/Entity';
|
||||||
|
import useFormRef from 'hooks/useFormRef';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
import { Entity } from 'models/Entity';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityDetails = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const [formKey, setFormKey] = React.useState(uuid());
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const [editing, setEditing] = useBoolean();
|
||||||
|
const { form, formRef } = useFormRef<Entity>();
|
||||||
|
const updateEntity = useUpdateEntity({ id });
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setFormKey(uuid());
|
||||||
|
}, [editing]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Heading my="auto" size="md">
|
||||||
|
{t('common.details')}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<SaveButton
|
||||||
|
onClick={form.submitForm}
|
||||||
|
isLoading={form.isSubmitting}
|
||||||
|
isCompact
|
||||||
|
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||||
|
hidden={!editing}
|
||||||
|
/>
|
||||||
|
<ToggleEditButton
|
||||||
|
toggleEdit={setEditing.toggle}
|
||||||
|
isEditing={editing}
|
||||||
|
isDisabled={getEntity.isFetching}
|
||||||
|
isDirty={form.dirty}
|
||||||
|
isCompact
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<Box w="100%">
|
||||||
|
{getEntity.data ? (
|
||||||
|
<Formik
|
||||||
|
innerRef={formRef}
|
||||||
|
enableReinitialize
|
||||||
|
key={formKey}
|
||||||
|
initialValues={getEntity.data}
|
||||||
|
validationSchema={EntitySchema(t)}
|
||||||
|
onSubmit={({ name, description, sourceIP, deviceRules }, { setSubmitting, resetForm }) =>
|
||||||
|
updateEntity.mutateAsync(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
deviceRules,
|
||||||
|
sourceIP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setSubmitting(false);
|
||||||
|
toast({
|
||||||
|
id: 'entity-update-success',
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('crud.success_update_obj', {
|
||||||
|
obj: t('entities.one'),
|
||||||
|
}),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
resetForm();
|
||||||
|
setEditing.off();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
id: uuid(),
|
||||||
|
title: t('common.error'),
|
||||||
|
description: t('crud.error_update_obj', {
|
||||||
|
obj: t('entities.one'),
|
||||||
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<Grid templateRows="repeat(1, 1fr)" templateColumns="repeat(3, 1fr)" gap={4}>
|
||||||
|
<GridItem colSpan={1}>
|
||||||
|
<StringField name="name" label={t('common.name')} isDisabled={!editing} isRequired />
|
||||||
|
</GridItem>
|
||||||
|
<GridItem colSpan={2}>
|
||||||
|
<StringField name="description" label={t('common.description')} isDisabled={!editing} />
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
<SimpleGrid minChildWidth="200px" spacing={4} mt={2}>
|
||||||
|
<IpDetectionModalField name="sourceIP" isDisabled={!editing} />
|
||||||
|
<DeviceRulesField isDisabled={!editing} />
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>{t('common.modified')}</FormLabel>
|
||||||
|
<Box pt={1.5}>
|
||||||
|
<FormattedDate date={getEntity.data?.modified} />
|
||||||
|
</Box>
|
||||||
|
</FormControl>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
) : (
|
||||||
|
<Center my={6}>
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityDetails;
|
||||||
@@ -19,26 +19,21 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
import { ContactObj } from 'models/Contact';
|
||||||
import { axiosProv } from 'utils/axiosInstances';
|
import { axiosProv } from 'utils/axiosInstances';
|
||||||
|
|
||||||
const deleteApi = async (id) => axiosProv.delete(`/contact/${id}`).then(() => true);
|
const deleteApi = async (id: string) => axiosProv.delete(`/contact/${id}`).then(() => true);
|
||||||
|
|
||||||
const propTypes = {
|
type Props = {
|
||||||
cell: PropTypes.shape({
|
contact: ContactObj;
|
||||||
original: PropTypes.shape({
|
refreshEntity: () => void;
|
||||||
id: PropTypes.string.isRequired,
|
openEditModal: (contact: ContactObj) => void;
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
entity: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
refreshEntity: PropTypes.func.isRequired,
|
|
||||||
openEditModal: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Actions = ({ cell: { original: contact }, refreshEntity, openEditModal }) => {
|
const ContactActions = ({ contact, refreshEntity, openEditModal }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
@@ -64,7 +59,7 @@ const Actions = ({ cell: { original: contact }, refreshEntity, openEditModal })
|
|||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
description: t('crud.error_delete_obj', {
|
description: t('crud.error_delete_obj', {
|
||||||
obj: contact.name,
|
obj: contact.name,
|
||||||
e: e?.response?.data?.ErrorDescription,
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
}),
|
}),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
@@ -83,7 +78,7 @@ const Actions = ({ cell: { original: contact }, refreshEntity, openEditModal })
|
|||||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||||
<Box>
|
<Box>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -107,12 +102,17 @@ const Actions = ({ cell: { original: contact }, refreshEntity, openEditModal })
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<IconButton ml={2} colorScheme="blue" icon={<MagnifyingGlass size={20} />} size="sm" onClick={handleOpenEdit} />
|
<IconButton
|
||||||
|
aria-label={t('common.view_details')}
|
||||||
|
ml={2}
|
||||||
|
colorScheme="blue"
|
||||||
|
icon={<MagnifyingGlass size={20} />}
|
||||||
|
size="sm"
|
||||||
|
onClick={handleOpenEdit}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Actions.propTypes = propTypes;
|
export default ContactActions;
|
||||||
|
|
||||||
export default Actions;
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Spacer, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import ContactActions from './ContactActions';
|
||||||
|
import CardBody from 'components/Card/CardBody';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import ContactTable from 'components/Tables/ContactTable';
|
||||||
|
import CreateContactModal from 'components/Tables/ContactTable/CreateContactModal';
|
||||||
|
import EditContactModal from 'components/Tables/ContactTable/EditContactModal';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
import { ContactObj } from 'models/Contact';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityContacts = ({ id }: Props) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const [contact, setContact] = React.useState<ContactObj>();
|
||||||
|
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||||
|
|
||||||
|
const openEditModal = (newContact: ContactObj) => {
|
||||||
|
setContact(newContact);
|
||||||
|
openEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetchContacts = () => {
|
||||||
|
queryClient.invalidateQueries(['get-contacts-select']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = React.useCallback(
|
||||||
|
(cell: { row: { original: ContactObj } }) => (
|
||||||
|
<ContactActions contact={cell.row.original} refreshEntity={getEntity.refetch} openEditModal={openEditModal} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={2} pt={2}>
|
||||||
|
<Spacer />
|
||||||
|
<CreateContactModal refresh={getEntity.refetch} entityId={getEntity.data?.id ?? ''} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box w="100%" overflowX="auto">
|
||||||
|
<ContactTable
|
||||||
|
select={getEntity.data?.contacts ?? []}
|
||||||
|
actions={actions}
|
||||||
|
ignoredColumns={['email', 'entity']}
|
||||||
|
openDetailsModal={openEditModal}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
<EditContactModal isOpen={isEditOpen} onClose={closeEdit} contact={contact} refresh={refetchContacts} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityContacts;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Spacer, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import LocationActions from './LocationActions';
|
||||||
|
import CardBody from 'components/Card/CardBody';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import LocationTable from 'components/Tables/LocationTable';
|
||||||
|
import CreateLocationModal from 'components/Tables/LocationTable/CreateLocationModal';
|
||||||
|
import EditLocationModal from 'components/Tables/LocationTable/EditLocationModal';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
import { Location } from 'models/Location';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityLocations = ({ id }: Props) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const [location, setLocation] = React.useState<Location>();
|
||||||
|
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||||
|
|
||||||
|
const openEditModal = (newLoc: Location) => {
|
||||||
|
setLocation(newLoc);
|
||||||
|
openEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetchLocations = () => {
|
||||||
|
queryClient.invalidateQueries(['get-locations-select']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = React.useCallback(
|
||||||
|
(cell: { row: { original: Location } }) => (
|
||||||
|
<LocationActions location={cell.row.original} refreshEntity={getEntity.refetch} openEditModal={openEditModal} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={2} pt={2}>
|
||||||
|
<Spacer />
|
||||||
|
<CreateLocationModal refresh={getEntity.refetch} entityId={getEntity.data?.id ?? ''} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box w="100%" overflowX="auto">
|
||||||
|
<LocationTable
|
||||||
|
select={getEntity.data?.locations ?? []}
|
||||||
|
actions={actions}
|
||||||
|
ignoredColumns={['entity']}
|
||||||
|
openDetailsModal={openEditModal}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
<EditLocationModal isOpen={isEditOpen} onClose={closeEdit} location={location} refresh={refetchLocations} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityLocations;
|
||||||
@@ -19,26 +19,21 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
import { Location } from 'models/Location';
|
||||||
import { axiosProv } from 'utils/axiosInstances';
|
import { axiosProv } from 'utils/axiosInstances';
|
||||||
|
|
||||||
const deleteApi = async (id) => axiosProv.delete(`/location/${id}`).then(() => true);
|
const deleteApi = async (id: string) => axiosProv.delete(`/location/${id}`).then(() => true);
|
||||||
|
|
||||||
const propTypes = {
|
type Props = {
|
||||||
cell: PropTypes.shape({
|
location: Location;
|
||||||
original: PropTypes.shape({
|
refreshEntity: () => void;
|
||||||
id: PropTypes.string.isRequired,
|
openEditModal: (location: Location) => void;
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
entity: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
refreshEntity: PropTypes.func.isRequired,
|
|
||||||
openEditModal: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Actions = ({ cell: { original: location }, refreshEntity, openEditModal }) => {
|
const LocationActions = ({ location, refreshEntity, openEditModal }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
@@ -64,7 +59,7 @@ const Actions = ({ cell: { original: location }, refreshEntity, openEditModal })
|
|||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
description: t('crud.error_delete_obj', {
|
description: t('crud.error_delete_obj', {
|
||||||
obj: location.name,
|
obj: location.name,
|
||||||
e: e?.response?.data?.ErrorDescription,
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
}),
|
}),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
@@ -83,7 +78,7 @@ const Actions = ({ cell: { original: location }, refreshEntity, openEditModal })
|
|||||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||||
<Box>
|
<Box>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -107,12 +102,17 @@ const Actions = ({ cell: { original: location }, refreshEntity, openEditModal })
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<IconButton ml={2} colorScheme="blue" icon={<MagnifyingGlass size={20} />} size="sm" onClick={handleOpenEdit} />
|
<IconButton
|
||||||
|
aria-label={t('common.view_details')}
|
||||||
|
ml={2}
|
||||||
|
colorScheme="blue"
|
||||||
|
icon={<MagnifyingGlass size={20} />}
|
||||||
|
size="sm"
|
||||||
|
onClick={handleOpenEdit}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Actions.propTypes = propTypes;
|
export default LocationActions;
|
||||||
|
|
||||||
export default Actions;
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import EntityContacts from './EntityContacts';
|
||||||
|
import EntityLocations from './EntityLocations';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityLocationContactsCard = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card p={0}>
|
||||||
|
<Tabs variant="enclosed" isLazy>
|
||||||
|
<TabList>
|
||||||
|
<CardHeader>
|
||||||
|
<Tab>{t('locations.title')}</Tab>
|
||||||
|
<Tab>{t('contacts.other')}</Tab>
|
||||||
|
</CardHeader>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel p={0}>
|
||||||
|
<Box
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderRight="1px solid"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
|
borderBottomLeftRadius="15px"
|
||||||
|
borderBottomRightRadius="15px"
|
||||||
|
>
|
||||||
|
<EntityLocations id={id} />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel p={0}>
|
||||||
|
<Box
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderRight="1px solid"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
|
borderBottomLeftRadius="15px"
|
||||||
|
borderBottomRightRadius="15px"
|
||||||
|
>
|
||||||
|
<EntityContacts id={id} />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityLocationContactsCard;
|
||||||
172
src/pages/EntityPage/Layout/EntityNotes.tsx
Normal file
172
src/pages/EntityPage/Layout/EntityNotes.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTrigger,
|
||||||
|
Spacer,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
useBreakpoint,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Plus } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardBody from 'components/Card/CardBody';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
|
import DataTable from 'components/DataTable';
|
||||||
|
import FormattedDate from 'components/FormattedDate';
|
||||||
|
import { useGetEntity, useUpdateEntity } from 'hooks/Network/Entity';
|
||||||
|
import { Note } from 'models/Note';
|
||||||
|
import { Column } from 'models/Table';
|
||||||
|
|
||||||
|
const EntityNotes = ({ id }: { id: string }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const [newNote, setNewNote] = React.useState('');
|
||||||
|
const updateEntity = useUpdateEntity({ id });
|
||||||
|
const toast = useToast();
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
const onNoteChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setNewNote(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onNoteSubmit = React.useCallback(
|
||||||
|
(onClose: () => void) => () => {
|
||||||
|
updateEntity.mutateAsync(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
notes: [{ note: newNote, created: 0 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
id: 'entity-update-success',
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('entities.update_success'),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
setNewNote('');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[newNote],
|
||||||
|
);
|
||||||
|
|
||||||
|
const notes = React.useMemo(
|
||||||
|
() => getEntity.data?.notes?.sort(({ created: a }, { created: b }) => b - a) ?? [],
|
||||||
|
[getEntity.data, getEntity.data?.notes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateCell = React.useCallback((created: number) => <FormattedDate date={created} />, []);
|
||||||
|
const noteCell = React.useCallback(
|
||||||
|
(note: string) => (
|
||||||
|
<Text w="100%" overflowWrap="break-word" whiteSpace="pre-wrap">
|
||||||
|
{note}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: Column<Note>[] = React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'created',
|
||||||
|
Header: t('common.date'),
|
||||||
|
Footer: '',
|
||||||
|
accessor: 'created',
|
||||||
|
Cell: ({ cell }: { cell: { row: { original: { created: number } } } }) => dateCell(cell.row.original.created),
|
||||||
|
customWidth: '150px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'note',
|
||||||
|
Header: t('common.note'),
|
||||||
|
Cell: ({ cell }: { cell: { row: { original: { note: string } } } }) => noteCell(cell.row.original.note),
|
||||||
|
Footer: '',
|
||||||
|
accessor: 'note',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'by',
|
||||||
|
Header: t('common.by'),
|
||||||
|
Footer: '',
|
||||||
|
accessor: 'createdBy',
|
||||||
|
customWidth: '200px',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[dateCell],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Card p={4}>
|
||||||
|
<CardHeader mb={2}>
|
||||||
|
<Heading size="md" my="auto">
|
||||||
|
{t('common.notes')}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
<Popover trigger="click" placement="auto">
|
||||||
|
{({ onClose }) => (
|
||||||
|
<>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton aria-label={`${t('crud.add')} ${t('common.note')}`} icon={<Plus size={20} />} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
|
<PopoverHeader display="flex">{t('profile.add_new_note')}</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
<Box>
|
||||||
|
<Textarea h="100px" placeholder="Your new note" value={newNote} onChange={onNoteChange} />
|
||||||
|
</Box>
|
||||||
|
<Center mt={2}>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
isDisabled={newNote.length === 0}
|
||||||
|
onClick={onNoteSubmit(onClose)}
|
||||||
|
isLoading={updateEntity.isLoading}
|
||||||
|
>
|
||||||
|
{t('crud.add')}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody display="block">
|
||||||
|
<Box overflow="auto" h="300px">
|
||||||
|
<DataTable
|
||||||
|
columns={columns as Column<object>[]}
|
||||||
|
data={notes}
|
||||||
|
obj={t('common.notes')}
|
||||||
|
sortBy={[
|
||||||
|
{
|
||||||
|
id: 'created',
|
||||||
|
desc: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
minHeight="200px"
|
||||||
|
hideControls
|
||||||
|
showAllRows
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityNotes;
|
||||||
@@ -32,20 +32,17 @@ interface Props {
|
|||||||
onOpenUpgradeModal: (serialNumber: string) => void;
|
onOpenUpgradeModal: (serialNumber: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Actions = (
|
const EntityInventoryActions = ({
|
||||||
{
|
cell: { original: tag },
|
||||||
cell: { original: tag },
|
refreshEntity,
|
||||||
refreshEntity,
|
openEditModal,
|
||||||
openEditModal,
|
onOpenScan,
|
||||||
onOpenScan,
|
onOpenFactoryReset,
|
||||||
onOpenFactoryReset,
|
onOpenUpgradeModal,
|
||||||
onOpenUpgradeModal
|
}: Props) => {
|
||||||
}: Props
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const { data: gwUi } = useGetGatewayUi();
|
const { data: gwUi } = useGetGatewayUi();
|
||||||
|
|
||||||
const { mutateAsync: deleteConfig, isLoading: isDeleting } = useDeleteTag({
|
const { mutateAsync: deleteConfig, isLoading: isDeleting } = useDeleteTag({
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
refreshTable: refreshEntity,
|
refreshTable: refreshEntity,
|
||||||
@@ -94,7 +91,7 @@ const Actions = (
|
|||||||
/>
|
/>
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="View Details"
|
aria-label="Open Edit"
|
||||||
ml={2}
|
ml={2}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
icon={<MagnifyingGlass size={20} />}
|
icon={<MagnifyingGlass size={20} />}
|
||||||
@@ -104,7 +101,7 @@ const Actions = (
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip hasArrow label={t('common.view_in_gateway')} placement="top">
|
<Tooltip hasArrow label={t('common.view_in_gateway')} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="View in Gateway"
|
aria-label="Go to gateway"
|
||||||
ml={2}
|
ml={2}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
icon={<ArrowSquareOut size={20} />}
|
icon={<ArrowSquareOut size={20} />}
|
||||||
@@ -116,4 +113,4 @@ const Actions = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Actions;
|
export default EntityInventoryActions;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import * as React from 'react';
|
||||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
import { Box, Heading, Spacer, useDisclosure } from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Actions from './Actions';
|
import EntityInventoryActions from './Actions';
|
||||||
|
import Card from 'components/Card';
|
||||||
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
import FactoryResetModal from 'components/Modals/SubscriberDevice/FactoryResetModal';
|
import FactoryResetModal from 'components/Modals/SubscriberDevice/FactoryResetModal';
|
||||||
import FirmwareUpgradeModal from 'components/Modals/SubscriberDevice/FirmwareUpgradeModal';
|
import FirmwareUpgradeModal from 'components/Modals/SubscriberDevice/FirmwareUpgradeModal';
|
||||||
import WifiScanModal from 'components/Modals/SubscriberDevice/WifiScanModal';
|
import WifiScanModal from 'components/Modals/SubscriberDevice/WifiScanModal';
|
||||||
@@ -11,19 +13,20 @@ import ConfigurationPushModal from 'components/Tables/InventoryTable/Configurati
|
|||||||
import CreateTagModal from 'components/Tables/InventoryTable/CreateTagModal';
|
import CreateTagModal from 'components/Tables/InventoryTable/CreateTagModal';
|
||||||
import EditTagModal from 'components/Tables/InventoryTable/EditTagModal';
|
import EditTagModal from 'components/Tables/InventoryTable/EditTagModal';
|
||||||
import ImportDeviceCsvModal from 'components/Tables/InventoryTable/ImportDeviceCsvModal';
|
import ImportDeviceCsvModal from 'components/Tables/InventoryTable/ImportDeviceCsvModal';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
import { usePushConfig } from 'hooks/Network/Inventory';
|
import { usePushConfig } from 'hooks/Network/Inventory';
|
||||||
import { Device } from 'models/Device';
|
import { Device } from 'models/Device';
|
||||||
import { Entity } from 'models/Entity';
|
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
entity: Entity;
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const EntityDeviceTableWrapper = ({ entity }: Props) => {
|
const EntityInventoryCard = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [tag, setTag] = useState<Device | undefined>(undefined);
|
const [tag, setTag] = React.useState<Device | undefined>(undefined);
|
||||||
const [serialNumber, setSerialNumber] = useState<string>('');
|
const [serialNumber, setSerialNumber] = React.useState<string>('');
|
||||||
const [refreshId, setRefreshId] = useState(0);
|
|
||||||
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
const { isOpen: isEditOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||||
const { isOpen: isPushOpen, onOpen: openPush, onClose: closePush } = useDisclosure();
|
const { isOpen: isPushOpen, onOpen: openPush, onClose: closePush } = useDisclosure();
|
||||||
const scanModalProps = useDisclosure();
|
const scanModalProps = useDisclosure();
|
||||||
@@ -48,38 +51,46 @@ const EntityDeviceTableWrapper = ({ entity }: Props) => {
|
|||||||
openEdit();
|
openEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshEntity = () => queryClient.invalidateQueries(['get-entity', entity.id]);
|
const actions = React.useCallback(
|
||||||
|
(cell: { row: { original: Device } }) => (
|
||||||
const refetchTags = () => setRefreshId(refreshId + 1);
|
<EntityInventoryActions
|
||||||
|
|
||||||
const actions = useCallback(
|
|
||||||
(cell) => (
|
|
||||||
<Actions
|
|
||||||
key={uuid()}
|
|
||||||
cell={cell.row}
|
cell={cell.row}
|
||||||
refreshEntity={refreshEntity}
|
refreshEntity={getEntity.refetch}
|
||||||
openEditModal={openEditModal}
|
openEditModal={openEditModal}
|
||||||
onOpenScan={onOpenScan}
|
onOpenScan={onOpenScan}
|
||||||
onOpenFactoryReset={onOpenFactoryReset}
|
onOpenFactoryReset={onOpenFactoryReset}
|
||||||
onOpenUpgradeModal={onOpenUpgradeModal}
|
onOpenUpgradeModal={onOpenUpgradeModal}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[refreshId],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refetchTags = React.useCallback(() => {
|
||||||
|
queryClient.invalidateQueries(['get-inventory-with-select']);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Card>
|
||||||
<Box textAlign="right" mb={2}>
|
<CardHeader>
|
||||||
<ImportDeviceCsvModal refresh={refreshEntity} parent={{ entity: entity.id }} deviceClass="entity" />
|
<Heading size="md" my="auto">
|
||||||
<CreateTagModal refresh={refreshEntity} entityId={`entity:${entity.id}`} deviceClass="entity" />
|
{t('inventory.title')}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
<ImportDeviceCsvModal
|
||||||
|
refresh={getEntity.refetch}
|
||||||
|
parent={{ entity: getEntity.data?.id }}
|
||||||
|
deviceClass="entity"
|
||||||
|
/>
|
||||||
|
<CreateTagModal refresh={getEntity.refetch} entityId={`entity:${getEntity.data?.id}`} deviceClass="entity" />
|
||||||
|
</CardHeader>
|
||||||
|
<Box overflowX="auto">
|
||||||
|
<InventoryTable
|
||||||
|
tagSelect={getEntity.data?.devices ?? []}
|
||||||
|
ignoredColumns={['entity', 'venue', 'description']}
|
||||||
|
actions={actions}
|
||||||
|
openDetailsModal={openEditModal}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<InventoryTable
|
|
||||||
tagSelect={entity.devices}
|
|
||||||
ignoredColumns={['entity', 'venue']}
|
|
||||||
refreshId={refreshId}
|
|
||||||
actions={actions}
|
|
||||||
openDetailsModal={openEditModal}
|
|
||||||
/>
|
|
||||||
<EditTagModal
|
<EditTagModal
|
||||||
isOpen={isEditOpen}
|
isOpen={isEditOpen}
|
||||||
onClose={closeEdit}
|
onClose={closeEdit}
|
||||||
@@ -94,8 +105,8 @@ const EntityDeviceTableWrapper = ({ entity }: Props) => {
|
|||||||
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
||||||
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
||||||
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
||||||
</>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EntityDeviceTableWrapper;
|
export default EntityInventoryCard;
|
||||||
31
src/pages/EntityPage/Layout/index.tsx
Normal file
31
src/pages/EntityPage/Layout/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Masonry from 'react-masonry-css';
|
||||||
|
import ConfigurationCard from './ConfigurationCard';
|
||||||
|
import EntityDetails from './EntityDetails';
|
||||||
|
import EntityLocationContactsCard from './EntityLocationContactsCard';
|
||||||
|
import EntityNotes from './EntityNotes';
|
||||||
|
import EntityInventoryCard from './InventoryCard';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityPageLayout = ({ id }: Props) => (
|
||||||
|
<Masonry
|
||||||
|
breakpointCols={{
|
||||||
|
default: 3,
|
||||||
|
2200: 2,
|
||||||
|
1100: 1,
|
||||||
|
}}
|
||||||
|
className="my-masonry-grid"
|
||||||
|
columnClassName="my-masonry-grid_column"
|
||||||
|
>
|
||||||
|
<EntityDetails id={id} />
|
||||||
|
<EntityInventoryCard id={id} />
|
||||||
|
<ConfigurationCard id={id} />
|
||||||
|
<EntityLocationContactsCard id={id} />
|
||||||
|
<EntityNotes id={id} />
|
||||||
|
</Masonry>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EntityPageLayout;
|
||||||
81
src/pages/EntityPage/VenueDropdown.tsx
Normal file
81
src/pages/EntityPage/VenueDropdown.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuDivider,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Tooltip,
|
||||||
|
useBreakpoint,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Buildings } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import CreateVenueModal from 'components/Tables/VenueTable/CreateVenueModal';
|
||||||
|
import { useGetEntity } from 'hooks/Network/Entity';
|
||||||
|
import { useGetSelectVenues } from 'hooks/Network/Venues';
|
||||||
|
import { Entity } from 'models/Entity';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VenueDropdown = ({ id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const getEntity = useGetEntity({ id });
|
||||||
|
const getChildren = useGetSelectVenues({ select: getEntity.data?.venues ?? [] });
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
const goToVenue = (venueId: string) => () => navigate(`/venue/${venueId}`);
|
||||||
|
|
||||||
|
const amount = getEntity.data?.venues.length ?? 0;
|
||||||
|
|
||||||
|
const isCompact = breakpoint === 'base' || breakpoint === 'sm';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu>
|
||||||
|
<Tooltip label={`${t('venues.sub_other')} (${amount})`}>
|
||||||
|
{isCompact ? (
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
icon={<Buildings size={20} />}
|
||||||
|
aria-label={`${t('venues.sub_other')} (${amount})`}
|
||||||
|
colorScheme="purple"
|
||||||
|
isDisabled={!getEntity.data}
|
||||||
|
ml={2}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
aria-label={`${t('venues.sub_other')} (${amount})`}
|
||||||
|
colorScheme="purple"
|
||||||
|
isDisabled={!getEntity.data}
|
||||||
|
>{`${t('venues.sub_other')} (${amount})`}</MenuButton>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={onOpen} isDisabled={id === '0000-0000-0000'}>
|
||||||
|
{id === '0000-0000-0000' ? t('entities.venues_under_root') : t('common.create')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider hidden={amount === 0} />
|
||||||
|
{getChildren.data
|
||||||
|
?.sort((a: Entity, b: Entity) => a.name.localeCompare(b.name))
|
||||||
|
.map(({ id: venueId, name }: Entity) => (
|
||||||
|
<MenuItem key={venueId} onClick={goToVenue(venueId)}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
)) ?? []}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
<CreateVenueModal isOpen={isOpen} onClose={onClose} entityId={getEntity.data?.id ?? ''} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VenueDropdown;
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import EntityCard from './EntityCard';
|
import EntityPageHeader from './EntityHeader';
|
||||||
import EntityChildrenCard from './EntityChildrenCard';
|
import EntityPageLayout from './Layout';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const EntityPage = ({ idToUse }: { idToUse?: string }) => {
|
const EntityPage = ({ idToUse }: { idToUse?: string }) => {
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
const entityIdToUse = React.useMemo(() => {
|
const entityIdToUse = React.useMemo(() => {
|
||||||
@@ -20,16 +17,12 @@ const EntityPage = ({ idToUse }: { idToUse?: string }) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [idToUse, id]);
|
}, [idToUse, id]);
|
||||||
|
|
||||||
return (
|
return entityIdToUse ? (
|
||||||
<Flex flexDirection="column" pt="75px">
|
<>
|
||||||
{isUserLoaded && entityIdToUse && (
|
<EntityPageHeader id={entityIdToUse} />
|
||||||
<>
|
<EntityPageLayout id={entityIdToUse} />
|
||||||
<EntityCard id={entityIdToUse} />
|
</>
|
||||||
<EntityChildrenCard id={entityIdToUse} />
|
) : null;
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EntityPage;
|
export default EntityPage;
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import Table from './Table';
|
import Table from './Table';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const InventoryPage = () => {
|
const InventoryPage = () => <Table />;
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
{isUserLoaded && <Table />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InventoryPage;
|
export default InventoryPage;
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import MapCard from './MapCard';
|
import MapCard from './MapCard';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const MapPage = () => {
|
const MapPage = () => <MapCard />;
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
{isUserLoaded && <MapCard />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MapPage;
|
export default MapPage;
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Heading } from '@chakra-ui/react';
|
import { Heading } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const NotFoundPage = () => {
|
const NotFoundPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return <Heading size="lg">{t('common.not_found')}</Heading>;
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
<Heading size="lg">{t('common.not_found')}</Heading>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotFoundPage;
|
export default NotFoundPage;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import FmsLogsCard from './FmsLogs';
|
import FmsLogsCard from './FmsLogs';
|
||||||
import GeneralLogsCard from './GeneralLogs';
|
import GeneralLogsCard from './GeneralLogs';
|
||||||
@@ -7,7 +7,6 @@ import LogsCard from './Notifications';
|
|||||||
import SecLogsCard from './SecLogs';
|
import SecLogsCard from './SecLogs';
|
||||||
import Card from 'components/Card';
|
import Card from 'components/Card';
|
||||||
import CardHeader from 'components/Card/CardHeader';
|
import CardHeader from 'components/Card/CardHeader';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const INDEX_PARAM = 'notifications-tab-index';
|
const INDEX_PARAM = 'notifications-tab-index';
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ const getDefaultTabIndex = () => {
|
|||||||
|
|
||||||
const NotificationsPage = () => {
|
const NotificationsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||||
|
|
||||||
const handleTabChange = (index: number) => {
|
const handleTabChange = (index: number) => {
|
||||||
@@ -31,74 +29,70 @@ const NotificationsPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" pt="75px">
|
<Card p={0}>
|
||||||
{isUserLoaded && (
|
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||||
<Card p={0}>
|
<TabList>
|
||||||
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
<CardHeader>
|
||||||
<TabList>
|
<Tab>
|
||||||
<CardHeader>
|
{t('venues.one')} {t('notification.other')}
|
||||||
<Tab>
|
</Tab>
|
||||||
{t('venues.one')} {t('notification.other')}
|
<Tab>Provisioning</Tab>
|
||||||
</Tab>
|
<Tab>{t('logs.security')}</Tab>
|
||||||
<Tab>Provisioning</Tab>
|
<Tab>{t('logs.firmware')}</Tab>
|
||||||
<Tab>{t('logs.security')}</Tab>
|
</CardHeader>
|
||||||
<Tab>{t('logs.firmware')}</Tab>
|
</TabList>
|
||||||
</CardHeader>
|
<TabPanels>
|
||||||
</TabList>
|
<TabPanel p={0}>
|
||||||
<TabPanels>
|
<Box
|
||||||
<TabPanel p={0}>
|
borderLeft="1px solid"
|
||||||
<Box
|
borderRight="1px solid"
|
||||||
borderLeft="1px solid"
|
borderBottom="1px solid"
|
||||||
borderRight="1px solid"
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
borderBottom="1px solid"
|
borderBottomLeftRadius="15px"
|
||||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
borderBottomRightRadius="15px"
|
||||||
borderBottomLeftRadius="15px"
|
>
|
||||||
borderBottomRightRadius="15px"
|
<LogsCard />
|
||||||
>
|
</Box>
|
||||||
<LogsCard />
|
</TabPanel>
|
||||||
</Box>
|
<TabPanel p={0}>
|
||||||
</TabPanel>
|
<Box
|
||||||
<TabPanel p={0}>
|
borderLeft="1px solid"
|
||||||
<Box
|
borderRight="1px solid"
|
||||||
borderLeft="1px solid"
|
borderBottom="1px solid"
|
||||||
borderRight="1px solid"
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
borderBottom="1px solid"
|
borderBottomLeftRadius="15px"
|
||||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
borderBottomRightRadius="15px"
|
||||||
borderBottomLeftRadius="15px"
|
>
|
||||||
borderBottomRightRadius="15px"
|
<GeneralLogsCard />
|
||||||
>
|
</Box>
|
||||||
<GeneralLogsCard />
|
</TabPanel>
|
||||||
</Box>
|
<TabPanel p={0}>
|
||||||
</TabPanel>
|
<Box
|
||||||
<TabPanel p={0}>
|
borderLeft="1px solid"
|
||||||
<Box
|
borderRight="1px solid"
|
||||||
borderLeft="1px solid"
|
borderBottom="1px solid"
|
||||||
borderRight="1px solid"
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
borderBottom="1px solid"
|
borderBottomLeftRadius="15px"
|
||||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
borderBottomRightRadius="15px"
|
||||||
borderBottomLeftRadius="15px"
|
>
|
||||||
borderBottomRightRadius="15px"
|
<SecLogsCard />
|
||||||
>
|
</Box>
|
||||||
<SecLogsCard />
|
</TabPanel>
|
||||||
</Box>
|
<TabPanel p={0}>
|
||||||
</TabPanel>
|
<Box
|
||||||
<TabPanel p={0}>
|
borderLeft="1px solid"
|
||||||
<Box
|
borderRight="1px solid"
|
||||||
borderLeft="1px solid"
|
borderBottom="1px solid"
|
||||||
borderRight="1px solid"
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
borderBottom="1px solid"
|
borderBottomLeftRadius="15px"
|
||||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
borderBottomRightRadius="15px"
|
||||||
borderBottomLeftRadius="15px"
|
>
|
||||||
borderBottomRightRadius="15px"
|
<FmsLogsCard />
|
||||||
>
|
</Box>
|
||||||
<FmsLogsCard />
|
</TabPanel>
|
||||||
</Box>
|
</TabPanels>
|
||||||
</TabPanel>
|
</Tabs>
|
||||||
</TabPanels>
|
</Card>
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import OperatorChildrenCard from './ChildrenCard';
|
import OperatorChildrenCard from './ChildrenCard';
|
||||||
import DetailsCard from './DetailsCard';
|
import DetailsCard from './DetailsCard';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const OperatorPage = ({ idToUse }: { idToUse?: string }) => {
|
const OperatorPage = ({ idToUse }: { idToUse?: string }) => {
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
const entityIdToUse = React.useMemo(() => {
|
const entityIdToUse = React.useMemo(() => {
|
||||||
@@ -20,16 +17,12 @@ const OperatorPage = ({ idToUse }: { idToUse?: string }) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [idToUse, id]);
|
}, [idToUse, id]);
|
||||||
|
|
||||||
return (
|
return entityIdToUse !== undefined ? (
|
||||||
<Flex flexDirection="column" pt="75px">
|
<>
|
||||||
{isUserLoaded && entityIdToUse !== undefined && (
|
<DetailsCard id={entityIdToUse} />
|
||||||
<>
|
<OperatorChildrenCard id={entityIdToUse} />
|
||||||
<DetailsCard id={entityIdToUse} />
|
</>
|
||||||
<OperatorChildrenCard id={entityIdToUse} />
|
) : null;
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OperatorPage;
|
export default OperatorPage;
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import OperatorsTable from './Table';
|
import OperatorsTable from './Table';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const OperatorsPage = () => {
|
const OperatorsPage = () => <OperatorsTable />;
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
{isUserLoaded && <OperatorsTable />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OperatorsPage;
|
export default OperatorsPage;
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Center, Flex, Spinner } from '@chakra-ui/react';
|
|
||||||
import ProfileLayout from './Layout';
|
import ProfileLayout from './Layout';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => <ProfileLayout />;
|
||||||
const context = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
{!context.isUserLoaded ? (
|
|
||||||
<Center mt={40}>
|
|
||||||
<Spinner size="xl" />
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<ProfileLayout />
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfilePage;
|
export default ProfilePage;
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import SubscriberCard from './SubscriberCard';
|
import SubscriberCard from './SubscriberCard';
|
||||||
import SubscriberChildrenCard from './SubscriberChildrenCard';
|
import SubscriberChildrenCard from './SubscriberChildrenCard';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const SubscriberPage = () => {
|
const SubscriberPage = () => {
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
return (
|
return id !== '' ? (
|
||||||
<Flex flexDirection="column" pt="75px">
|
<>
|
||||||
{isUserLoaded && id !== '' && (
|
<SubscriberCard id={id ?? ''} />
|
||||||
<>
|
<SubscriberChildrenCard id={id ?? ''} />
|
||||||
<SubscriberCard id={id ?? ''} />
|
</>
|
||||||
<SubscriberChildrenCard id={id ?? ''} />
|
) : null;
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SubscriberPage;
|
export default SubscriberPage;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Flex, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import RefreshButton from '../../components/Buttons/RefreshButton';
|
import RefreshButton from '../../components/Buttons/RefreshButton';
|
||||||
@@ -25,7 +25,7 @@ type Props = {
|
|||||||
|
|
||||||
const SystemPage = ({ isOnlySec }: Props) => {
|
const SystemPage = ({ isOnlySec }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { token, user, isUserLoaded } = useAuth();
|
const { token, user } = useAuth();
|
||||||
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
|
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
|
||||||
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||||
const handleTabChange = (index: number) => {
|
const handleTabChange = (index: number) => {
|
||||||
@@ -56,55 +56,51 @@ const SystemPage = ({ isOnlySec }: Props) => {
|
|||||||
.map((endpoint) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
|
.map((endpoint) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
|
||||||
}, [endpoints, token]);
|
}, [endpoints, token]);
|
||||||
|
|
||||||
if (!isUserLoaded) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" pt="75px">
|
<Card p={0}>
|
||||||
<Card p={0}>
|
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||||
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
<TabList>
|
||||||
<TabList>
|
<CardHeader>
|
||||||
<CardHeader>
|
<Tab>{t('system.services')}</Tab>
|
||||||
<Tab>{t('system.services')}</Tab>
|
<Tab hidden={!isRoot}>{t('system.configuration')}</Tab>
|
||||||
<Tab hidden={!isRoot}>{t('system.configuration')}</Tab>
|
</CardHeader>
|
||||||
</CardHeader>
|
</TabList>
|
||||||
</TabList>
|
<TabPanels>
|
||||||
<TabPanels>
|
<TabPanel p={0}>
|
||||||
<TabPanel p={0}>
|
<Box
|
||||||
<Box
|
borderLeft="1px solid"
|
||||||
borderLeft="1px solid"
|
borderRight="1px solid"
|
||||||
borderRight="1px solid"
|
borderBottom="1px solid"
|
||||||
borderBottom="1px solid"
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
borderBottomLeftRadius="15px"
|
||||||
borderBottomLeftRadius="15px"
|
borderBottomRightRadius="15px"
|
||||||
borderBottomRightRadius="15px"
|
>
|
||||||
>
|
{!isOnlySec && (
|
||||||
{!isOnlySec && (
|
<CardHeader px={4} pt={4}>
|
||||||
<CardHeader px={4} pt={4}>
|
<Spacer />
|
||||||
<Spacer />
|
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
</CardHeader>
|
||||||
</CardHeader>
|
)}
|
||||||
)}
|
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
|
||||||
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
|
{endpointsList}
|
||||||
{endpointsList}
|
</SimpleGrid>
|
||||||
</SimpleGrid>
|
</Box>
|
||||||
</Box>
|
</TabPanel>
|
||||||
</TabPanel>
|
<TabPanel p={0}>
|
||||||
<TabPanel p={0}>
|
<Box
|
||||||
<Box
|
borderLeft="1px solid"
|
||||||
borderLeft="1px solid"
|
borderRight="1px solid"
|
||||||
borderRight="1px solid"
|
borderBottom="1px solid"
|
||||||
borderBottom="1px solid"
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
borderBottomLeftRadius="15px"
|
||||||
borderBottomLeftRadius="15px"
|
borderBottomRightRadius="15px"
|
||||||
borderBottomRightRadius="15px"
|
>
|
||||||
>
|
<SystemSecretsCard />
|
||||||
<SystemSecretsCard />
|
</Box>
|
||||||
</Box>
|
</TabPanel>
|
||||||
</TabPanel>
|
</TabPanels>
|
||||||
</TabPanels>
|
</Tabs>
|
||||||
</Tabs>
|
</Card>
|
||||||
</Card>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
|
||||||
import UserTable from './Table';
|
import UserTable from './Table';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
|
|
||||||
const UsersPage = () => {
|
const UsersPage = () => <UserTable />;
|
||||||
const { isUserLoaded } = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex flexDirection="column" pt="75px">
|
|
||||||
{isUserLoaded && <UserTable />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UsersPage;
|
export default UsersPage;
|
||||||
|
|||||||
194
src/pages/VenuePage/Layout/AnalyticsCard/DatePickers.tsx
Normal file
194
src/pages/VenuePage/Layout/AnalyticsCard/DatePickers.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTrigger,
|
||||||
|
Spacer,
|
||||||
|
Tooltip,
|
||||||
|
useBreakpoint,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Clock, Prohibit } from 'phosphor-react';
|
||||||
|
import ReactDatePicker from 'react-datepicker';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import CloseButton from 'components/Buttons/CloseButton';
|
||||||
|
import SaveButton from 'components/Buttons/SaveButton';
|
||||||
|
|
||||||
|
const CustomInputButton = React.forwardRef(
|
||||||
|
({ value, onClick }: { value: string; onClick: () => void }, ref: React.LegacyRef<HTMLButtonElement>) => (
|
||||||
|
<Button colorScheme="gray" size="sm" onClick={onClick} ref={ref} mt={1}>
|
||||||
|
{value}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStart = () => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setHours(date.getHours() - 1);
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaults?: { start: Date; end: Date };
|
||||||
|
setTime: (start: Date, end: Date) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnalyticsDatePickers = ({ defaults, setTime, onClear }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [start, setStart] = React.useState<Date>(defaults?.start ?? getStart());
|
||||||
|
const [end, setEnd] = React.useState<Date>(defaults?.end ?? new Date());
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
const onStartChange = (newDate: Date) => {
|
||||||
|
setStart(newDate);
|
||||||
|
};
|
||||||
|
const onEndChange = (newDate: Date) => {
|
||||||
|
setEnd(newDate);
|
||||||
|
};
|
||||||
|
const clear = (onClose: () => void) => () => {
|
||||||
|
onClear();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
const onSave = (onClose: () => void) => () => {
|
||||||
|
onClose();
|
||||||
|
setTime(start, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
const width = (isOpen: boolean) => {
|
||||||
|
if (isOpen) {
|
||||||
|
return breakpoint === 'base' ? '360px' : '460px';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setStart(defaults?.start ?? getStart());
|
||||||
|
setEnd(defaults?.end ?? new Date());
|
||||||
|
}, [defaults]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
{({ isOpen, onClose }) => (
|
||||||
|
<>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Box>
|
||||||
|
<Tooltip label={t('controller.crud.choose_time')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('controller.crud.choose_time')}
|
||||||
|
icon={<Clock size={20} />}
|
||||||
|
mx={2}
|
||||||
|
colorScheme="blue"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent w={width(isOpen)}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverHeader display="flex">
|
||||||
|
<Heading size="sm" my="auto">
|
||||||
|
{t('controller.crud.choose_time')}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
<HStack>
|
||||||
|
<Tooltip label={t('controller.crud.clear_time')}>
|
||||||
|
<IconButton
|
||||||
|
colorScheme="red"
|
||||||
|
aria-label={t('controller.crud.clear_time')}
|
||||||
|
onClick={clear(onClose)}
|
||||||
|
icon={<Prohibit size={20} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<SaveButton onClick={onSave(onClose)} isCompact />
|
||||||
|
<CloseButton onClick={onClose} />
|
||||||
|
</HStack>
|
||||||
|
</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
{breakpoint === 'base' ? (
|
||||||
|
<Box>
|
||||||
|
<Flex>
|
||||||
|
<Heading size="sm" my="auto" mr={2}>
|
||||||
|
{t('system.start')}:{' '}
|
||||||
|
</Heading>
|
||||||
|
<Box w="170px">
|
||||||
|
<ReactDatePicker
|
||||||
|
selected={start}
|
||||||
|
onChange={onStartChange}
|
||||||
|
timeInputLabel={`${t('common.time')}: `}
|
||||||
|
dateFormat="dd/MM/yyyy hh:mm aa"
|
||||||
|
timeFormat="p"
|
||||||
|
showTimeSelect
|
||||||
|
// @ts-ignore
|
||||||
|
customInput={<CustomInputButton />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Heading size="sm" my="auto" mr={4}>
|
||||||
|
{t('common.end')}:{' '}
|
||||||
|
</Heading>
|
||||||
|
<Box w="170px">
|
||||||
|
<ReactDatePicker
|
||||||
|
selected={end}
|
||||||
|
onChange={onEndChange}
|
||||||
|
timeInputLabel={`${t('common.time')}: `}
|
||||||
|
dateFormat="dd/MM/yyyy hh:mm aa"
|
||||||
|
timeFormat="p"
|
||||||
|
showTimeSelect
|
||||||
|
// @ts-ignore
|
||||||
|
customInput={<CustomInputButton />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Flex>
|
||||||
|
<Heading size="sm" my="auto" mr={2}>
|
||||||
|
{t('system.start')}:{' '}
|
||||||
|
</Heading>
|
||||||
|
<Box w="170px">
|
||||||
|
<ReactDatePicker
|
||||||
|
selected={start}
|
||||||
|
onChange={onStartChange}
|
||||||
|
timeInputLabel={`${t('common.time')}: `}
|
||||||
|
dateFormat="dd/MM/yyyy hh:mm aa"
|
||||||
|
timeFormat="p"
|
||||||
|
showTimeSelect
|
||||||
|
// @ts-ignore
|
||||||
|
customInput={<CustomInputButton />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Heading size="sm" my="auto" mr={2}>
|
||||||
|
{t('common.end')}:{' '}
|
||||||
|
</Heading>
|
||||||
|
<Box w="170px">
|
||||||
|
<ReactDatePicker
|
||||||
|
selected={end}
|
||||||
|
onChange={onEndChange}
|
||||||
|
timeInputLabel={`${t('common.time')}: `}
|
||||||
|
dateFormat="dd/MM/yyyy hh:mm aa"
|
||||||
|
timeFormat="p"
|
||||||
|
showTimeSelect
|
||||||
|
// @ts-ignore
|
||||||
|
customInput={<CustomInputButton />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalyticsDatePickers;
|
||||||
@@ -18,24 +18,33 @@ import {
|
|||||||
Thead,
|
Thead,
|
||||||
Tr,
|
Tr,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { animated } from '@react-spring/web';
|
import { ComputedDatum } from '@nivo/circle-packing';
|
||||||
|
import { Interpolation, SpringValue, animated } from '@react-spring/web';
|
||||||
import { WifiHigh } from 'phosphor-react';
|
import { WifiHigh } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AssociationCircle } from '../utils';
|
||||||
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
||||||
import { bytesString, formatNumberToScientificBasedOnMax } from 'utils/stringHelper';
|
import { bytesString, formatNumberToScientificBasedOnMax } from 'utils/stringHelper';
|
||||||
|
|
||||||
const propTypes = {
|
const AssociationCirclePack = ({
|
||||||
node: PropTypes.instanceOf(Object).isRequired,
|
node,
|
||||||
handleClicks: PropTypes.shape({
|
style,
|
||||||
onClick: PropTypes.func.isRequired,
|
handleClicks,
|
||||||
}).isRequired,
|
}: {
|
||||||
style: PropTypes.instanceOf(Object).isRequired,
|
node: ComputedDatum<AssociationCircle>;
|
||||||
};
|
style: {
|
||||||
|
x: SpringValue<number>;
|
||||||
const AssociationCircle = ({ node, style, handleClicks }) => {
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
};
|
||||||
|
handleClicks: {
|
||||||
|
onClick: (e: React.MouseEvent<SVGCircleElement>) => void;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { popoverRef } = useCircleGraph();
|
const context = useCircleGraph();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isLazy trigger="hover" placement="auto">
|
<Popover isLazy trigger="hover" placement="auto">
|
||||||
@@ -53,7 +62,7 @@ const AssociationCircle = ({ node, style, handleClicks }) => {
|
|||||||
onClick={handleClicks.onClick}
|
onClick={handleClicks.onClick}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal containerRef={popoverRef}>
|
<Portal containerRef={context?.popoverRef}>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverCloseButton alignContent="center" mt={1} />
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
@@ -117,5 +126,4 @@ const AssociationCircle = ({ node, style, handleClicks }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AssociationCircle.propTypes = propTypes;
|
export default AssociationCirclePack;
|
||||||
export default React.memo(AssociationCircle);
|
|
||||||
@@ -19,27 +19,36 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Tag as TagDisplay,
|
Tag as TagDisplay,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { animated } from '@react-spring/web';
|
import { ComputedDatum } from '@nivo/circle-packing';
|
||||||
|
import { Interpolation, SpringValue, animated } from '@react-spring/web';
|
||||||
import { ArrowSquareOut, Tag } from 'phosphor-react';
|
import { ArrowSquareOut, Tag } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DeviceCircleInfo } from '../utils';
|
||||||
import FormattedDate from 'components/FormattedDate';
|
import FormattedDate from 'components/FormattedDate';
|
||||||
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
||||||
import { useGetGatewayUi } from 'hooks/Network/Endpoints';
|
import { useGetGatewayUi } from 'hooks/Network/Endpoints';
|
||||||
import { bytesString } from 'utils/stringHelper';
|
import { bytesString } from 'utils/stringHelper';
|
||||||
|
|
||||||
const propTypes = {
|
const DeviceCirclePack = ({
|
||||||
node: PropTypes.instanceOf(Object).isRequired,
|
node,
|
||||||
handleClicks: PropTypes.shape({
|
style,
|
||||||
onClick: PropTypes.func.isRequired,
|
handleClicks,
|
||||||
}).isRequired,
|
}: {
|
||||||
style: PropTypes.instanceOf(Object).isRequired,
|
node: ComputedDatum<DeviceCircleInfo>;
|
||||||
};
|
style: {
|
||||||
|
x: SpringValue<number>;
|
||||||
const DeviceCircle = ({ node, style, handleClicks }) => {
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
};
|
||||||
|
handleClicks: {
|
||||||
|
onClick: (e: React.MouseEvent<SVGCircleElement>) => void;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: gwUi } = useGetGatewayUi();
|
const { data: gwUi } = useGetGatewayUi();
|
||||||
const { popoverRef } = useCircleGraph();
|
const context = useCircleGraph();
|
||||||
|
|
||||||
const handleOpenInGateway = useMemo(
|
const handleOpenInGateway = useMemo(
|
||||||
() => () => window.open(`${gwUi}/#/devices/${node.data.details.deviceInfo.serialNumber}`, '_blank'),
|
() => () => window.open(`${gwUi}/#/devices/${node.data.details.deviceInfo.serialNumber}`, '_blank'),
|
||||||
@@ -62,7 +71,7 @@ const DeviceCircle = ({ node, style, handleClicks }) => {
|
|||||||
onClick={handleClicks.onClick}
|
onClick={handleClicks.onClick}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal containerRef={popoverRef}>
|
<Portal containerRef={context?.popoverRef}>
|
||||||
<PopoverContent w="580px">
|
<PopoverContent w="580px">
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverCloseButton alignContent="center" mt={1} />
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
@@ -71,6 +80,7 @@ const DeviceCircle = ({ node, style, handleClicks }) => {
|
|||||||
<Text ml={2}>{node?.data?.name.split('/')[0]}</Text>
|
<Text ml={2}>{node?.data?.name.split('/')[0]}</Text>
|
||||||
<Tooltip hasArrow label={t('common.view_in_gateway')} placement="top">
|
<Tooltip hasArrow label={t('common.view_in_gateway')} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label={t('common.view_in_gateway')}
|
||||||
ml={2}
|
ml={2}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
icon={<ArrowSquareOut size={20} />}
|
icon={<ArrowSquareOut size={20} />}
|
||||||
@@ -91,13 +101,13 @@ const DeviceCircle = ({ node, style, handleClicks }) => {
|
|||||||
: node.data.details.deviceInfo.deviceType}
|
: node.data.details.deviceInfo.deviceType}
|
||||||
</Td>
|
</Td>
|
||||||
<Td w="150px">TX {t('analytics.delta')}</Td>
|
<Td w="150px">TX {t('analytics.delta')}</Td>
|
||||||
<Td>{bytesString(node.data.details.tx_bytes_delta)}</Td>
|
<Td>{bytesString(node.data.details.apData.tx_bytes_delta)}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td w="130px">{t('analytics.firmware')}</Td>
|
<Td w="130px">{t('analytics.firmware')}</Td>
|
||||||
<Td>{node.data.details.deviceInfo.lastFirmware?.split('/')[1] ?? t('common.unknown')}</Td>
|
<Td>{node.data.details.deviceInfo.lastFirmware?.split('/')[1] ?? t('common.unknown')}</Td>
|
||||||
<Td w="150px">RX {t('analytics.delta')}</Td>
|
<Td w="150px">RX {t('analytics.delta')}</Td>
|
||||||
<Td>{bytesString(node.data.details.rx_bytes_delta)}</Td>
|
<Td>{bytesString(node.data.details.apData.rx_bytes_delta)}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td w="130px">SSIDs</Td>
|
<Td w="130px">SSIDs</Td>
|
||||||
@@ -139,5 +149,4 @@ const DeviceCircle = ({ node, style, handleClicks }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DeviceCircle.propTypes = propTypes;
|
export default DeviceCirclePack;
|
||||||
export default React.memo(DeviceCircle);
|
|
||||||
@@ -16,23 +16,32 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Tag,
|
Tag,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { animated } from '@react-spring/web';
|
import { ComputedDatum } from '@nivo/circle-packing';
|
||||||
|
import { Interpolation, SpringValue, animated } from '@react-spring/web';
|
||||||
import { Radio } from 'phosphor-react';
|
import { Radio } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RadioCircle } from '../utils';
|
||||||
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
||||||
|
|
||||||
const propTypes = {
|
const RadioCirclePack = ({
|
||||||
node: PropTypes.instanceOf(Object).isRequired,
|
node,
|
||||||
handleClicks: PropTypes.shape({
|
style,
|
||||||
onClick: PropTypes.func.isRequired,
|
handleClicks,
|
||||||
}).isRequired,
|
}: {
|
||||||
style: PropTypes.instanceOf(Object).isRequired,
|
node: ComputedDatum<RadioCircle>;
|
||||||
};
|
style: {
|
||||||
|
x: SpringValue<number>;
|
||||||
const RadioCircle = ({ node, style, handleClicks }) => {
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
};
|
||||||
|
handleClicks: {
|
||||||
|
onClick: (e: React.MouseEvent<SVGCircleElement>) => void;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { popoverRef } = useCircleGraph();
|
const context = useCircleGraph();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isLazy trigger="hover" placement="auto">
|
<Popover isLazy trigger="hover" placement="auto">
|
||||||
@@ -50,7 +59,7 @@ const RadioCircle = ({ node, style, handleClicks }) => {
|
|||||||
onClick={handleClicks.onClick}
|
onClick={handleClicks.onClick}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal containerRef={popoverRef}>
|
<Portal containerRef={context?.popoverRef}>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverCloseButton alignContent="center" mt={1} />
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
@@ -76,7 +85,7 @@ const RadioCircle = ({ node, style, handleClicks }) => {
|
|||||||
<Td w="100px">{t('analytics.airtime')}</Td>
|
<Td w="100px">{t('analytics.airtime')}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Tag ml={-2} colorScheme={node.data.details.tagColor} size="md">
|
<Tag ml={-2} colorScheme={node.data.details.tagColor} size="md">
|
||||||
<b>{node.data.details.transmitPct.toFixed(2)}%</b>
|
<b>{node.data.details.transmit_pct.toFixed(2)}%</b>
|
||||||
</Tag>
|
</Tag>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
@@ -106,5 +115,4 @@ const RadioCircle = ({ node, style, handleClicks }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
RadioCircle.propTypes = propTypes;
|
export default RadioCirclePack;
|
||||||
export default React.memo(RadioCircle);
|
|
||||||
@@ -19,24 +19,33 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Tag,
|
Tag,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { animated } from '@react-spring/web';
|
import { ComputedDatum } from '@nivo/circle-packing';
|
||||||
|
import { Interpolation, SpringValue, animated } from '@react-spring/web';
|
||||||
import { Broadcast } from 'phosphor-react';
|
import { Broadcast } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SsidCircle } from '../utils';
|
||||||
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
||||||
import { bytesString } from 'utils/stringHelper';
|
import { bytesString } from 'utils/stringHelper';
|
||||||
|
|
||||||
const propTypes = {
|
const SsidCirclePack = ({
|
||||||
node: PropTypes.instanceOf(Object).isRequired,
|
node,
|
||||||
handleClicks: PropTypes.shape({
|
style,
|
||||||
onClick: PropTypes.func.isRequired,
|
handleClicks,
|
||||||
}).isRequired,
|
}: {
|
||||||
style: PropTypes.instanceOf(Object).isRequired,
|
node: ComputedDatum<SsidCircle>;
|
||||||
};
|
style: {
|
||||||
|
x: SpringValue<number>;
|
||||||
const SsidCircle = ({ node, style, handleClicks }) => {
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
};
|
||||||
|
handleClicks: {
|
||||||
|
onClick: (e: React.MouseEvent<SVGCircleElement>) => void;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { popoverRef } = useCircleGraph();
|
const context = useCircleGraph();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isLazy trigger="hover" placement="auto">
|
<Popover isLazy trigger="hover" placement="auto">
|
||||||
@@ -55,7 +64,7 @@ const SsidCircle = ({ node, style, handleClicks }) => {
|
|||||||
onClick={handleClicks.onClick}
|
onClick={handleClicks.onClick}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal containerRef={popoverRef}>
|
<Portal containerRef={context?.popoverRef}>
|
||||||
<PopoverContent w="400px">
|
<PopoverContent w="400px">
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverCloseButton alignContent="center" mt={1} />
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
@@ -122,5 +131,4 @@ const SsidCircle = ({ node, style, handleClicks }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
SsidCircle.propTypes = propTypes;
|
export default SsidCirclePack;
|
||||||
export default React.memo(SsidCircle);
|
|
||||||
@@ -12,23 +12,32 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { animated } from '@react-spring/web';
|
import { ComputedDatum } from '@nivo/circle-packing';
|
||||||
|
import { Interpolation, SpringValue, animated } from '@react-spring/web';
|
||||||
import { Buildings } from 'phosphor-react';
|
import { Buildings } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CirclePackRoot } from '../utils';
|
||||||
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
||||||
|
|
||||||
const propTypes = {
|
const VenueCirclePack = ({
|
||||||
node: PropTypes.instanceOf(Object).isRequired,
|
node,
|
||||||
handleClicks: PropTypes.shape({
|
style,
|
||||||
onClick: PropTypes.func.isRequired,
|
handleClicks,
|
||||||
}).isRequired,
|
}: {
|
||||||
style: PropTypes.instanceOf(Object).isRequired,
|
node: ComputedDatum<CirclePackRoot>;
|
||||||
};
|
style: {
|
||||||
|
x: SpringValue<number>;
|
||||||
const VenueCircle = ({ node, style, handleClicks }) => {
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
};
|
||||||
|
handleClicks: {
|
||||||
|
onClick: (e: React.MouseEvent<SVGCircleElement>) => void;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { popoverRef } = useCircleGraph();
|
const context = useCircleGraph();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isLazy trigger="hover" placement="auto">
|
<Popover isLazy trigger="hover" placement="auto">
|
||||||
@@ -46,7 +55,7 @@ const VenueCircle = ({ node, style, handleClicks }) => {
|
|||||||
onClick={handleClicks.onClick}
|
onClick={handleClicks.onClick}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal containerRef={popoverRef}>
|
<Portal containerRef={context?.popoverRef}>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverCloseButton alignContent="center" mt={1} />
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
@@ -72,5 +81,4 @@ const VenueCircle = ({ node, style, handleClicks }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
VenueCircle.propTypes = propTypes;
|
export default VenueCirclePack;
|
||||||
export default React.memo(VenueCircle);
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { ComputedDatum, CircleComponent as CircleComponentT, CircleProps } from '@nivo/circle-packing';
|
||||||
|
import { Interpolation, SpringValue } from '@react-spring/web';
|
||||||
|
import { AssociationCircle, CirclePackRoot, DeviceCircleInfo, RadioCircle, SsidCircle } from '../utils';
|
||||||
|
import AssociationCirclePack from './AssociationCirclePack';
|
||||||
|
import DeviceCirclePack from './DeviceCirclePack';
|
||||||
|
import RadioCirclePack from './RadioCirclePack';
|
||||||
|
import SsidCirclePack from './SsidCirclePack';
|
||||||
|
import VenueCirclePack from './VenueCirclePack';
|
||||||
|
|
||||||
|
const CircleComponent: CircleComponentT<
|
||||||
|
CirclePackRoot | SsidCircle | RadioCircle | AssociationCircle | DeviceCircleInfo
|
||||||
|
> = ({
|
||||||
|
node,
|
||||||
|
style,
|
||||||
|
onClick,
|
||||||
|
}: CircleProps<CirclePackRoot | SsidCircle | RadioCircle | AssociationCircle | DeviceCircleInfo>) => {
|
||||||
|
const handleClicks = useMemo(
|
||||||
|
() => ({
|
||||||
|
onClick: (e: React.MouseEvent<SVGCircleElement>) => {
|
||||||
|
if (onClick) onClick(node, e);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onClick, node],
|
||||||
|
);
|
||||||
|
if (node.data.type === 'association')
|
||||||
|
return (
|
||||||
|
<AssociationCirclePack
|
||||||
|
node={node as ComputedDatum<AssociationCircle>}
|
||||||
|
style={
|
||||||
|
style as unknown as {
|
||||||
|
x: SpringValue<number>;
|
||||||
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClicks={handleClicks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (node.data.type === 'ssid')
|
||||||
|
return (
|
||||||
|
<SsidCirclePack
|
||||||
|
node={node as ComputedDatum<SsidCircle>}
|
||||||
|
style={
|
||||||
|
style as unknown as {
|
||||||
|
x: SpringValue<number>;
|
||||||
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClicks={handleClicks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (node.data.type === 'radio')
|
||||||
|
return (
|
||||||
|
<RadioCirclePack
|
||||||
|
node={node as ComputedDatum<RadioCircle>}
|
||||||
|
style={
|
||||||
|
style as unknown as {
|
||||||
|
x: SpringValue<number>;
|
||||||
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClicks={handleClicks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (node.data.type === 'device')
|
||||||
|
return (
|
||||||
|
<DeviceCirclePack
|
||||||
|
node={node as ComputedDatum<DeviceCircleInfo>}
|
||||||
|
style={
|
||||||
|
style as unknown as {
|
||||||
|
x: SpringValue<number>;
|
||||||
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClicks={handleClicks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (node.data.type === 'venue')
|
||||||
|
return (
|
||||||
|
<VenueCirclePack
|
||||||
|
node={node as ComputedDatum<CirclePackRoot>}
|
||||||
|
style={
|
||||||
|
style as unknown as {
|
||||||
|
x: SpringValue<number>;
|
||||||
|
y: SpringValue<number>;
|
||||||
|
radius: Interpolation<number>;
|
||||||
|
textColor: SpringValue<string>;
|
||||||
|
opacity: SpringValue<number>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClicks={handleClicks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CircleComponent;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LabelComponent, LabelProps } from '@nivo/circle-packing';
|
||||||
|
import { animated } from '@react-spring/web';
|
||||||
|
import { CirclePackRoot } from './utils';
|
||||||
|
|
||||||
|
const CircleLabel: LabelComponent<CirclePackRoot> = ({ label, node, style }: LabelProps<CirclePackRoot>) => (
|
||||||
|
<animated.text
|
||||||
|
key={node.id}
|
||||||
|
x={style.x}
|
||||||
|
y={style.y}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
style={{
|
||||||
|
fill: style.textColor,
|
||||||
|
opacity: style.opacity,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof label === 'string' ? label.split('/')[0] : label}
|
||||||
|
</animated.text>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CircleLabel;
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Box, Center, Heading, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Tooltip } from '@chakra-ui/react';
|
import { Box, Center, Heading, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Tooltip } from '@chakra-ui/react';
|
||||||
import { Clock } from 'phosphor-react';
|
import { Clock } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
import { AnalyticsTimePointApiResponse } from 'models/Analytics';
|
||||||
import { compactDate } from 'utils/dateFormatting';
|
import { compactDate } from 'utils/dateFormatting';
|
||||||
|
|
||||||
const propTypes = {
|
type Props = {
|
||||||
index: PropTypes.number.isRequired,
|
index: number;
|
||||||
setIndex: PropTypes.func.isRequired,
|
setIndex: (index: number) => void;
|
||||||
points: PropTypes.instanceOf(Object).isRequired,
|
points: AnalyticsTimePointApiResponse[][];
|
||||||
};
|
};
|
||||||
|
const CirclePackSlider = ({ index, setIndex, points }: Props) => {
|
||||||
const CirclePackSlider = ({ index, setIndex, points }) => {
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
const onMouseEnter = () => setShowTooltip(true);
|
const onMouseEnter = () => setShowTooltip(true);
|
||||||
@@ -19,11 +18,13 @@ const CirclePackSlider = ({ index, setIndex, points }) => {
|
|||||||
const stepsDetails = useMemo(
|
const stepsDetails = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
steps: points.length,
|
steps: points.length,
|
||||||
allTimestamps: points.map((point) => (point.length === 0 ? '-' : compactDate(point[0].timestamp))),
|
allTimestamps: points.map((point) => (!point[0] ? '-' : compactDate(point[0].timestamp))),
|
||||||
}),
|
}),
|
||||||
[points],
|
[points],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currTimestamp = points[index]?.[0]?.timestamp;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -46,11 +47,10 @@ const CirclePackSlider = ({ index, setIndex, points }) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Slider>
|
</Slider>
|
||||||
<Center>
|
<Center>
|
||||||
<Heading size="lg">{points[index] && points[index][0] ? compactDate(points[index][0].timestamp) : ''}</Heading>
|
<Heading size="lg">{currTimestamp ? compactDate(currTimestamp) : ''}</Heading>
|
||||||
</Center>
|
</Center>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CirclePackSlider.propTypes = propTypes;
|
|
||||||
export default React.memo(CirclePackSlider);
|
export default React.memo(CirclePackSlider);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Center, Heading, useColorMode } from '@chakra-ui/react';
|
||||||
|
import { MouseHandler, ResponsiveCirclePacking } from '@nivo/circle-packing';
|
||||||
|
import { FullScreenHandle } from 'react-full-screen';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import CircleComponent from './CircleComponent';
|
||||||
|
import CircleLabel from './CircleLabel';
|
||||||
|
import CirclePackSlider from './Slider';
|
||||||
|
import { useCirclePackTheme } from './useCirclePackTheme';
|
||||||
|
import { CirclePackRoot, parseAnalyticsTimepointsToCirclePackData } from './utils';
|
||||||
|
import { useCircleGraph } from 'contexts/CircleGraphProvider';
|
||||||
|
import { AnalyticsTimePointApiResponse } from 'models/Analytics';
|
||||||
|
import { VenueApiResponse } from 'models/Venue';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: AnalyticsTimePointApiResponse[][];
|
||||||
|
venue: VenueApiResponse;
|
||||||
|
handle: FullScreenHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LiveViewCirclePack = ({ data, venue, handle }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const context = useCircleGraph();
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const theme = useCirclePackTheme();
|
||||||
|
const [pointIndex, setPointIndex] = React.useState(Math.max(data.length - 1, 0));
|
||||||
|
const [zoomedId, setZoomedId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const parsedData = React.useMemo(() => {
|
||||||
|
const dataIndex = data[pointIndex] || [];
|
||||||
|
|
||||||
|
if (dataIndex) {
|
||||||
|
try {
|
||||||
|
return parseAnalyticsTimepointsToCirclePackData(dataIndex, venue, colorMode);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [data, pointIndex, colorMode]);
|
||||||
|
|
||||||
|
const handleNodeClick: MouseHandler<CirclePackRoot> = React.useCallback(
|
||||||
|
(node) => {
|
||||||
|
setZoomedId(zoomedId === node.id ? null : node.id);
|
||||||
|
},
|
||||||
|
[zoomedId],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setPointIndex(data.length - 1);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={10} h="100%">
|
||||||
|
{data.length > 0 && <CirclePackSlider index={pointIndex} setIndex={setPointIndex} points={data} />}
|
||||||
|
<Box w="100%" h={handle?.active ? 'calc(100vh - 200px)' : '600px'} ref={context?.popoverRef}>
|
||||||
|
{!parsedData ? (
|
||||||
|
<Center>
|
||||||
|
<Heading size="lg">{t('common.no_records_found')}</Heading>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<ResponsiveCirclePacking
|
||||||
|
margin={theme.MARGINS}
|
||||||
|
padding={36}
|
||||||
|
defs={theme.shapeDefs}
|
||||||
|
animate={false}
|
||||||
|
fill={theme.getFill}
|
||||||
|
id="name"
|
||||||
|
value="scale"
|
||||||
|
data={parsedData}
|
||||||
|
enableLabels
|
||||||
|
labelsSkipRadius={42}
|
||||||
|
labelsFilter={theme.getLabelsFilter}
|
||||||
|
labelTextColor={theme.LABEL_TEXT_COLORS}
|
||||||
|
labelComponent={CircleLabel}
|
||||||
|
// onMouseEnter={null}
|
||||||
|
// tooltip={null}
|
||||||
|
circleComponent={CircleComponent}
|
||||||
|
zoomedId={zoomedId}
|
||||||
|
theme={theme.THEME}
|
||||||
|
onClick={handleNodeClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveViewCirclePack;
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useColorMode } from '@chakra-ui/react';
|
||||||
|
import { patternLinesDef } from '@nivo/core';
|
||||||
|
import { AssociationCircle, CirclePackRoot, RadioCircle, SsidCircle } from './utils';
|
||||||
|
|
||||||
|
const THEME = {
|
||||||
|
labels: {
|
||||||
|
text: {
|
||||||
|
background: 'black',
|
||||||
|
},
|
||||||
|
background: 'black',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const LABEL_TEXT_COLORS = {
|
||||||
|
from: 'color',
|
||||||
|
modifiers: [['darker', 4]],
|
||||||
|
};
|
||||||
|
|
||||||
|
const MARGINS = { top: 20, right: 20, bottom: 20, left: 20 };
|
||||||
|
|
||||||
|
const getFill = [
|
||||||
|
{
|
||||||
|
match: (d: { data: CirclePackRoot | SsidCircle | RadioCircle | AssociationCircle }) =>
|
||||||
|
d.data.type === 'association' && typeof d.data.details.rssi === 'number' && d.data.details.rssi >= -45,
|
||||||
|
id: 'assoc_success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: (d: { data: CirclePackRoot | SsidCircle | RadioCircle | AssociationCircle }) =>
|
||||||
|
d.data.type === 'association' && typeof d.data.details.rssi === 'number' && d.data.details.rssi >= -60,
|
||||||
|
id: 'assoc_warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: (d: { data: CirclePackRoot | SsidCircle | RadioCircle | AssociationCircle }) =>
|
||||||
|
d.data.type === 'association' && typeof d.data.details.rssi === 'number' && d.data.details.rssi < -60,
|
||||||
|
id: 'assoc_danger',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getLabelsFilter = (label: { node: { height: number } }) => label.node.height === 0;
|
||||||
|
|
||||||
|
export const useCirclePackTheme = () => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
|
const shapeDefs = React.useMemo(
|
||||||
|
() => [
|
||||||
|
patternLinesDef(
|
||||||
|
'assoc_success',
|
||||||
|
colorMode === 'light'
|
||||||
|
? {
|
||||||
|
rotation: -45,
|
||||||
|
color: 'var(--chakra-colors-success-400)',
|
||||||
|
background: 'var(--chakra-colors-success-600)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
rotation: -45,
|
||||||
|
color: 'var(--chakra-colors-success-400)',
|
||||||
|
background: 'var(--chakra-colors-success-600)',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
patternLinesDef(
|
||||||
|
'assoc_warning',
|
||||||
|
colorMode === 'light'
|
||||||
|
? {
|
||||||
|
rotation: -45,
|
||||||
|
color: 'var(--chakra-colors-warning-100)',
|
||||||
|
background: 'var(--chakra-colors-warning-400)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
rotation: -45,
|
||||||
|
color: 'var(--chakra-colors-warning-100)',
|
||||||
|
background: 'var(--chakra-colors-warning-400)',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
patternLinesDef(
|
||||||
|
'assoc_danger',
|
||||||
|
colorMode === 'light'
|
||||||
|
? {
|
||||||
|
rotation: -45,
|
||||||
|
color: 'var(--chakra-colors-danger-200)',
|
||||||
|
background: 'var(--chakra-colors-danger-400)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
rotation: -45,
|
||||||
|
color: 'var(--chakra-colors-danger-200)',
|
||||||
|
background: 'var(--chakra-colors-danger-400)',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[colorMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
shapeDefs,
|
||||||
|
THEME,
|
||||||
|
LABEL_TEXT_COLORS,
|
||||||
|
MARGINS,
|
||||||
|
getFill,
|
||||||
|
getLabelsFilter,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import {
|
||||||
|
AnalyticsApData,
|
||||||
|
AnalyticsAssociationData,
|
||||||
|
AnalyticsBoardDevice,
|
||||||
|
AnalyticsRadioData,
|
||||||
|
AnalyticsSsidData,
|
||||||
|
AnalyticsTimePointApiResponse,
|
||||||
|
} from 'models/Analytics';
|
||||||
|
import { VenueApiResponse } from 'models/Venue';
|
||||||
|
import { getScaledArray } from 'utils/arrayHelpers';
|
||||||
|
import { errorColor, getBlendedColor, successColor, warningColor } from 'utils/colors';
|
||||||
|
import { parseDbm } from 'utils/stringHelper';
|
||||||
|
|
||||||
|
type ChangeTypeOfKeys<T extends object, Keys extends keyof T, NewType> = {
|
||||||
|
// Loop to every key. We gonna check if the key
|
||||||
|
// is assignable to Keys. If yes, change the type.
|
||||||
|
// Else, retain the type.
|
||||||
|
[key in keyof T]: key extends Keys ? NewType : T[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CircleColor = 'green' | 'yellow' | 'red';
|
||||||
|
|
||||||
|
export type AssociationCircle = {
|
||||||
|
name: string;
|
||||||
|
type: 'association';
|
||||||
|
details: ChangeTypeOfKeys<AnalyticsAssociationData, 'rssi', number | '-'> & {
|
||||||
|
color: string;
|
||||||
|
tagColor: CircleColor;
|
||||||
|
};
|
||||||
|
scale: number;
|
||||||
|
totalBw: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SsidCircle = {
|
||||||
|
name: string;
|
||||||
|
type: 'ssid';
|
||||||
|
details: {
|
||||||
|
avgRssi: '-' | number;
|
||||||
|
color: string;
|
||||||
|
tagColor: CircleColor;
|
||||||
|
} & AnalyticsSsidData;
|
||||||
|
children: AssociationCircle[];
|
||||||
|
scale: number;
|
||||||
|
};
|
||||||
|
export type RadioCircle = {
|
||||||
|
name: string;
|
||||||
|
type: 'radio';
|
||||||
|
details: {
|
||||||
|
color: string;
|
||||||
|
tagColor: CircleColor;
|
||||||
|
} & AnalyticsRadioData;
|
||||||
|
children: SsidCircle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeviceCircleInfo = {
|
||||||
|
name: string;
|
||||||
|
type: 'device';
|
||||||
|
details: {
|
||||||
|
deviceInfo: AnalyticsBoardDevice;
|
||||||
|
ssidData: AnalyticsSsidData[];
|
||||||
|
apData: AnalyticsApData;
|
||||||
|
color: string;
|
||||||
|
tagColor: CircleColor;
|
||||||
|
};
|
||||||
|
scale: number;
|
||||||
|
children: RadioCircle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CirclePackRoot = {
|
||||||
|
name: string;
|
||||||
|
type: 'venue';
|
||||||
|
details: {
|
||||||
|
avgHealth: number;
|
||||||
|
color: string;
|
||||||
|
tagColor: CircleColor;
|
||||||
|
};
|
||||||
|
children: DeviceCircleInfo[];
|
||||||
|
scale: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseAnalyticsTimepointsToCirclePackData = (
|
||||||
|
data: AnalyticsTimePointApiResponse[],
|
||||||
|
venue: VenueApiResponse,
|
||||||
|
colorMode: 'light' | 'dark',
|
||||||
|
) => {
|
||||||
|
if (data.length === 0) return undefined;
|
||||||
|
|
||||||
|
const root: CirclePackRoot = {
|
||||||
|
name: venue.name,
|
||||||
|
details: {
|
||||||
|
avgHealth: 0,
|
||||||
|
color: 'green',
|
||||||
|
tagColor: 'green',
|
||||||
|
},
|
||||||
|
type: 'venue',
|
||||||
|
children: [],
|
||||||
|
scale: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let globalVenueHealth = 0;
|
||||||
|
const globalBandwidth: number[] = [];
|
||||||
|
|
||||||
|
for (const device of data) {
|
||||||
|
globalVenueHealth += device.device_info.health;
|
||||||
|
|
||||||
|
const deviceCircleInfo: DeviceCircleInfo = {
|
||||||
|
name: `${device.device_info.serialNumber}/device/${uuid()}`,
|
||||||
|
type: 'device',
|
||||||
|
details: {
|
||||||
|
deviceInfo: device.device_info,
|
||||||
|
ssidData: device.ssid_data,
|
||||||
|
apData: device.ap_data,
|
||||||
|
color: 'green',
|
||||||
|
tagColor: 'green',
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (device.device_info.health >= 90) {
|
||||||
|
deviceCircleInfo.details.color = successColor(colorMode);
|
||||||
|
deviceCircleInfo.details.tagColor = 'green';
|
||||||
|
} else if (device.device_info.health >= 70) {
|
||||||
|
deviceCircleInfo.details.color = warningColor(colorMode);
|
||||||
|
deviceCircleInfo.details.tagColor = 'yellow';
|
||||||
|
} else {
|
||||||
|
deviceCircleInfo.details.color = errorColor(colorMode);
|
||||||
|
deviceCircleInfo.details.tagColor = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
const radioChannelIndex: { [key: string]: number } = {};
|
||||||
|
|
||||||
|
for (const [i, radio] of device.radio_data.entries()) {
|
||||||
|
radioChannelIndex[radio.band] = i;
|
||||||
|
|
||||||
|
let tagColor: CircleColor = 'green';
|
||||||
|
if (radio.transmit_pct > 30) tagColor = 'yellow';
|
||||||
|
else if (radio.transmit_pct > 50) tagColor = 'red';
|
||||||
|
|
||||||
|
deviceCircleInfo.children.push({
|
||||||
|
name: `${radio.band}/radio/${uuid()}`,
|
||||||
|
type: 'radio',
|
||||||
|
details: {
|
||||||
|
...radio,
|
||||||
|
color: getBlendedColor('#0ba057', '#FD3049', radio.transmit_pct / 100),
|
||||||
|
tagColor,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ssid of device.ssid_data) {
|
||||||
|
const ssidInfo: SsidCircle = {
|
||||||
|
name: `${ssid.ssid}/ssid/${uuid()}`,
|
||||||
|
type: 'ssid',
|
||||||
|
details: {
|
||||||
|
...ssid,
|
||||||
|
avgRssi: '-',
|
||||||
|
color: 'green',
|
||||||
|
tagColor: 'green',
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
scale: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalSsidRssi = 0;
|
||||||
|
|
||||||
|
for (const association of ssid.associations) {
|
||||||
|
const bw = association.tx_bytes_bw + association.rx_bytes_bw;
|
||||||
|
globalBandwidth.push(bw);
|
||||||
|
|
||||||
|
const associationInfo: AssociationCircle = {
|
||||||
|
name: `${association.station}/assoc/${uuid()}`,
|
||||||
|
type: 'association',
|
||||||
|
details: {
|
||||||
|
...association,
|
||||||
|
rssi: parseDbm(association.rssi) as '-' | number,
|
||||||
|
color: 'green',
|
||||||
|
tagColor: 'green',
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
totalBw: bw,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (association.rssi >= -45) {
|
||||||
|
associationInfo.details.color = successColor(colorMode);
|
||||||
|
associationInfo.details.tagColor = 'green';
|
||||||
|
} else if (association.rssi >= -60) {
|
||||||
|
associationInfo.details.color = warningColor(colorMode);
|
||||||
|
associationInfo.details.tagColor = 'yellow';
|
||||||
|
} else {
|
||||||
|
associationInfo.details.color = errorColor(colorMode);
|
||||||
|
associationInfo.details.tagColor = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSsidRssi += association.rssi;
|
||||||
|
ssidInfo.children.push(associationInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = radioChannelIndex[ssid.band];
|
||||||
|
if (index !== undefined) {
|
||||||
|
ssidInfo.details.avgRssi =
|
||||||
|
ssid.associations.length === 0
|
||||||
|
? '-'
|
||||||
|
: parseDbm(Math.floor(totalSsidRssi / Math.max(ssid.associations.length, 1)));
|
||||||
|
if (typeof ssidInfo.details.avgRssi === 'number') {
|
||||||
|
if (ssid.associations.length === 0 || ssidInfo.details.avgRssi >= -45) {
|
||||||
|
ssidInfo.details.color = successColor(colorMode);
|
||||||
|
ssidInfo.details.tagColor = 'green';
|
||||||
|
} else if (ssidInfo.details.avgRssi >= -60) {
|
||||||
|
ssidInfo.details.color = warningColor(colorMode);
|
||||||
|
ssidInfo.details.tagColor = 'yellow';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ssidInfo.details.color = errorColor(colorMode);
|
||||||
|
ssidInfo.details.tagColor = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceCircleInfo.children[index]?.children.push(ssidInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.details.avgHealth = Math.floor(globalVenueHealth / Math.max(data.length, 1));
|
||||||
|
if (root.details.avgHealth >= 90) {
|
||||||
|
root.details.color = successColor(colorMode);
|
||||||
|
root.details.tagColor = 'green';
|
||||||
|
} else if (root.details.avgHealth >= 70) {
|
||||||
|
root.details.color = warningColor(colorMode);
|
||||||
|
root.details.tagColor = 'yellow';
|
||||||
|
} else {
|
||||||
|
root.details.color = errorColor(colorMode);
|
||||||
|
root.details.tagColor = 'red';
|
||||||
|
}
|
||||||
|
root.children.push(deviceCircleInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalBandwidth.length > 0) {
|
||||||
|
const scaledArray = getScaledArray(globalBandwidth, 1, 30);
|
||||||
|
const bandwidthObj: { [key: number]: number } = {};
|
||||||
|
for (const [i, bw] of globalBandwidth.entries()) {
|
||||||
|
bandwidthObj[bw] = scaledArray[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [deviceIndex, dev] of root.children.entries()) {
|
||||||
|
for (const [radioIndex, radio] of dev.children.entries()) {
|
||||||
|
for (const [ssidIndex, ssid] of radio.children.entries()) {
|
||||||
|
for (const [assocIndex, assoc] of ssid.children.entries()) {
|
||||||
|
if (root.children[deviceIndex]?.children[radioIndex]?.children[ssidIndex]?.children[assocIndex]?.scale)
|
||||||
|
// @ts-ignore
|
||||||
|
root.children[deviceIndex].children[radioIndex].children[ssidIndex].children[assocIndex].scale =
|
||||||
|
bandwidthObj[assoc.totalBw];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { ArrowsIn, ArrowsOut } from 'phosphor-react';
|
||||||
|
import { FullScreenHandle } from 'react-full-screen';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isDisabled: boolean;
|
||||||
|
handle: FullScreenHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FullScreenLiveViewButton = ({ isDisabled, handle }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleClick = () => (handle.active ? handle.exit() : handle.enter());
|
||||||
|
const icon = () => (handle.active ? <ArrowsIn size={20} /> : <ArrowsOut size={20} />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={handle.active ? t('common.exit_fullscreen') : t('common.fullscreen')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={handle.active ? t('common.exit_fullscreen') : t('common.fullscreen')}
|
||||||
|
type="button"
|
||||||
|
onClick={handleClick}
|
||||||
|
icon={icon()}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
colorScheme="teal"
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullScreenLiveViewButton;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTrigger,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Question } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const CirclePackInfoButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Tooltip label={t('configurations.explanation')}>
|
||||||
|
<IconButton aria-label={t('configurations.explanation')} icon={<Question size={20} />} colorScheme="blue" />
|
||||||
|
</Tooltip>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent w="440px">
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
|
<PopoverHeader display="flex">{t('analytics.live_view_help')}</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
<Heading size="sm">{t('analytics.live_view_explanation_one')}</Heading>
|
||||||
|
<Heading size="sm" mt={4}>
|
||||||
|
{t('analytics.live_view_explanation_two')}
|
||||||
|
</Heading>
|
||||||
|
<Heading size="sm">{t('analytics.live_view_explanation_three')}</Heading>
|
||||||
|
<Heading size="sm" mt={4}>
|
||||||
|
{t('analytics.live_view_explanation_four')}
|
||||||
|
</Heading>
|
||||||
|
<Heading size="sm" mt={4}>
|
||||||
|
{t('analytics.live_view_explanation_five')}
|
||||||
|
</Heading>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CirclePackInfoButton;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Flex, HStack, Spacer, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||||
|
import AnalyticsDatePickers from '../DatePickers';
|
||||||
|
import LiveViewCirclePack from './CirclePack';
|
||||||
|
import FullScreenLiveViewButton from './FullScreenLiveViewButton';
|
||||||
|
import CirclePackInfoButton from './InfoButton';
|
||||||
|
import { UseLiveViewReturn } from './useLiveView';
|
||||||
|
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||||
|
import LoadingOverlay from 'components/LoadingOverlay';
|
||||||
|
import { CircleGraphProvider } from 'contexts/CircleGraphProvider';
|
||||||
|
import { AnalyticsTimePointApiResponse } from 'models/Analytics';
|
||||||
|
import { VenueApiResponse } from 'models/Venue';
|
||||||
|
import { getHoursAgo } from 'utils/dateFormatting';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: AnalyticsTimePointApiResponse[][];
|
||||||
|
venue: VenueApiResponse;
|
||||||
|
isFetching?: boolean;
|
||||||
|
onChangeTime: UseLiveViewReturn['onChangeTime'];
|
||||||
|
onClearTime: UseLiveViewReturn['onClearTime'];
|
||||||
|
refreshData: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LiveViewLayout = ({ data, venue, isFetching, onChangeTime, onClearTime, refreshData }: Props) => {
|
||||||
|
const color = useColorModeValue('gray.50', 'gray.800');
|
||||||
|
const handle = useFullScreenHandle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingOverlay isLoading={!!isFetching}>
|
||||||
|
<Box bgColor={handle?.active ? color : undefined} h="100%" p={0}>
|
||||||
|
<FullScreen handle={handle}>
|
||||||
|
<Box bgColor={handle?.active ? color : undefined} h="100%" p={0}>
|
||||||
|
<Flex mb={2} pt={2} px={2} mt={handle?.active ? 4 : undefined} mr={handle?.active ? 4 : undefined}>
|
||||||
|
<Spacer />
|
||||||
|
<HStack>
|
||||||
|
<CirclePackInfoButton />
|
||||||
|
<AnalyticsDatePickers
|
||||||
|
defaults={{
|
||||||
|
start: getHoursAgo(5),
|
||||||
|
end: new Date(),
|
||||||
|
}}
|
||||||
|
setTime={(start: Date, end: Date) => onChangeTime({ start, end })}
|
||||||
|
onClear={onClearTime}
|
||||||
|
/>
|
||||||
|
<FullScreenLiveViewButton isDisabled={isFetching || !data} handle={handle} />
|
||||||
|
<RefreshButton onClick={refreshData} isFetching={isFetching} isCompact />
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
<CircleGraphProvider>
|
||||||
|
<LiveViewCirclePack data={data} handle={handle} venue={venue} />
|
||||||
|
</CircleGraphProvider>
|
||||||
|
</Box>
|
||||||
|
</FullScreen>
|
||||||
|
</Box>
|
||||||
|
</LoadingOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LiveViewLayout);
|
||||||
64
src/pages/VenuePage/Layout/AnalyticsCard/LiveView/index.tsx
Normal file
64
src/pages/VenuePage/Layout/AnalyticsCard/LiveView/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, BoxProps, Center, Flex, Spinner } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LiveViewLayout from './LiveViewLayout';
|
||||||
|
import { useLiveView } from './useLiveView';
|
||||||
|
import { VenueApiResponse } from 'models/Venue';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
boardId: string;
|
||||||
|
venue: VenueApiResponse;
|
||||||
|
containerStyle?: BoxProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LiveView = ({ boardId, venue, containerStyle }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const liveView = useLiveView({ boardId });
|
||||||
|
|
||||||
|
const contents = React.useMemo(() => {
|
||||||
|
if (liveView.getTimepoints.error) {
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="center" alignItems="center" height="100%">
|
||||||
|
<Center>
|
||||||
|
<Alert status="error" w="unset" borderRadius="15px">
|
||||||
|
<AlertIcon />
|
||||||
|
<Box>
|
||||||
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{liveView.getTimepoints.error.response?.status === 404
|
||||||
|
? t('analytics.missing_board')
|
||||||
|
: liveView.getTimepoints.error.response?.data?.ErrorDescription}
|
||||||
|
</AlertDescription>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveView.getTimepoints.isLoading || !liveView.getTimepoints.data) {
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="center" alignItems="center" height="100%">
|
||||||
|
<Center>
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LiveViewLayout
|
||||||
|
data={liveView.getTimepoints.data}
|
||||||
|
isFetching={liveView.getTimepoints.isFetching}
|
||||||
|
venue={venue}
|
||||||
|
onChangeTime={liveView.onChangeTime}
|
||||||
|
onClearTime={liveView.onClearTime}
|
||||||
|
refreshData={liveView.getTimepoints.refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [liveView.getTimepoints]);
|
||||||
|
|
||||||
|
return <Box {...containerStyle}>{contents}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LiveView);
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useGetAnalyticsBoardTimepoints } from 'hooks/Network/Analytics';
|
||||||
|
import { getHoursAgo } from 'utils/dateFormatting';
|
||||||
|
|
||||||
|
export type UseLiveViewProps = {
|
||||||
|
boardId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseLiveViewReturn = {
|
||||||
|
time: { start: Date; end: Date };
|
||||||
|
onChangeTime: (newTime: { start: Date; end: Date }) => void;
|
||||||
|
onClearTime: () => void;
|
||||||
|
getTimepoints: ReturnType<typeof useGetAnalyticsBoardTimepoints>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLiveView = ({ boardId }: UseLiveViewProps) => {
|
||||||
|
const [time, setTime] = React.useState({
|
||||||
|
start: getHoursAgo(5),
|
||||||
|
end: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChangeTime = (newTime: { start: Date; end: Date }) => setTime({ ...newTime });
|
||||||
|
const onClearTime = () => {
|
||||||
|
setTime({
|
||||||
|
start: getHoursAgo(5),
|
||||||
|
end: new Date(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimepoints = useGetAnalyticsBoardTimepoints({ id: boardId, startTime: time.start, endTime: time.end });
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
time,
|
||||||
|
onChangeTime,
|
||||||
|
onClearTime,
|
||||||
|
getTimepoints,
|
||||||
|
} as UseLiveViewReturn),
|
||||||
|
[getTimepoints, time],
|
||||||
|
);
|
||||||
|
};
|
||||||
186
src/pages/VenuePage/Layout/AnalyticsCard/StartAnalyticsModal.tsx
Normal file
186
src/pages/VenuePage/Layout/AnalyticsCard/StartAnalyticsModal.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, VStack, useDisclosure, useToast } from '@chakra-ui/react';
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import SaveButton from 'components/Buttons/SaveButton';
|
||||||
|
import NumberField from 'components/FormFields/NumberField';
|
||||||
|
import StringField from 'components/FormFields/StringField';
|
||||||
|
import ToggleField from 'components/FormFields/ToggleField';
|
||||||
|
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||||
|
import { Modal } from 'components/Modals/Modal';
|
||||||
|
import { testObjectName } from 'constants/formTests';
|
||||||
|
import { useCreateAnalyticsBoard } from 'hooks/Network/Analytics';
|
||||||
|
import { useGetVenue, useUpdateVenue } from 'hooks/Network/Venues';
|
||||||
|
import useFormRef from 'hooks/useFormRef';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
name: string;
|
||||||
|
interval: number;
|
||||||
|
retention: number;
|
||||||
|
monitorSubVenues: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StartAnalyticsModal = ({ id, isOpen, onClose }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const getVenue = useGetVenue({ id });
|
||||||
|
const createAnalytics = useCreateAnalyticsBoard();
|
||||||
|
const updateVenue = useUpdateVenue({ id });
|
||||||
|
const [formKey, setFormKey] = React.useState(uuid());
|
||||||
|
const { isOpen: isConfirmOpen, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||||
|
const { form, formRef } = useFormRef<FormValues>();
|
||||||
|
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||||
|
|
||||||
|
const Schema = Yup.object()
|
||||||
|
.shape({
|
||||||
|
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
|
||||||
|
interval: Yup.number().required(t('form.required')).moreThan(0).integer(),
|
||||||
|
retention: Yup.number().required(t('form.required')).moreThan(0).integer(),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.default(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setFormKey(uuid());
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title={t('analytics.create_board')}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeModal}
|
||||||
|
topRightButtons={
|
||||||
|
<SaveButton onClick={form.submitForm} isLoading={form.isSubmitting} isDisabled={!form.isValid} />
|
||||||
|
}
|
||||||
|
options={{
|
||||||
|
modalSize: 'sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Formik
|
||||||
|
innerRef={formRef}
|
||||||
|
enableReinitialize
|
||||||
|
key={formKey}
|
||||||
|
initialValues={
|
||||||
|
{
|
||||||
|
name: getVenue.data?.name ?? '',
|
||||||
|
interval: 60,
|
||||||
|
retention: 3600 * 24 * 7,
|
||||||
|
monitorSubVenues: true,
|
||||||
|
} as FormValues
|
||||||
|
}
|
||||||
|
validationSchema={Schema}
|
||||||
|
onSubmit={({ name, interval, retention, monitorSubVenues }, { setSubmitting, resetForm }) => {
|
||||||
|
createAnalytics.mutateAsync(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
venueList: [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
retention,
|
||||||
|
interval,
|
||||||
|
monitorSubVenues,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ data: boardData }) => {
|
||||||
|
updateVenue.mutateAsync(
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
boards: [boardData.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setSubmitting(false);
|
||||||
|
toast({
|
||||||
|
id: 'venue-update-success',
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('crud.success_update_obj', {
|
||||||
|
obj: t('venues.one'),
|
||||||
|
}),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
id: uuid(),
|
||||||
|
title: t('common.error'),
|
||||||
|
description: t('crud.error_update_obj', {
|
||||||
|
obj: t('venues.one'),
|
||||||
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
id: uuid(),
|
||||||
|
title: t('common.error'),
|
||||||
|
description: t('crud.error_create_obj', {
|
||||||
|
obj: t('analytics.board'),
|
||||||
|
e: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<VStack spacing={2} alignItems="left">
|
||||||
|
<StringField name="name" label={t('common.name')} isRequired />
|
||||||
|
<Box maxW="200px">
|
||||||
|
<NumberField name="interval" label={t('analytics.interval')} isRequired unit={t('common.seconds')} />
|
||||||
|
</Box>
|
||||||
|
<Box maxW="200px">
|
||||||
|
<NumberField
|
||||||
|
name="retention"
|
||||||
|
label={t('analytics.retention')}
|
||||||
|
isRequired
|
||||||
|
unit={t('common.days')}
|
||||||
|
conversionFactor={3600 * 24}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ToggleField name="monitorSubVenues" label={t('analytics.analyze_sub_venues')} />
|
||||||
|
</VStack>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmCloseAlert isOpen={isConfirmOpen} confirm={onClose} cancel={closeConfirm} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartAnalyticsModal;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
useDisclosure,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Stop } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDeleteAnalyticsBoard } from 'hooks/Network/Analytics';
|
||||||
|
import { useUpdateVenue } from 'hooks/Network/Venues';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
boardId: string;
|
||||||
|
venueId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StopMonitoringButton = ({ boardId, venueId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const stopMonitoring = useDeleteAnalyticsBoard();
|
||||||
|
const updateVenue = useUpdateVenue({ id: venueId });
|
||||||
|
const cancelRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const handleStop = () => {
|
||||||
|
updateVenue.mutate(
|
||||||
|
{ params: { boards: [] } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
stopMonitoring.mutate(boardId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('analytics.stop_monitoring_success'),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
title: t('common.error'),
|
||||||
|
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
title: t('common.error'),
|
||||||
|
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip label={t('analytics.stop_monitoring')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('analytics.stop_monitoring')}
|
||||||
|
icon={<Stop size={20} />}
|
||||||
|
colorScheme="red"
|
||||||
|
borderRadius={0}
|
||||||
|
onClick={onOpen}
|
||||||
|
h="41px"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose} isCentered>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
{t('analytics.stop_monitoring')}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>{t('analytics.stop_monitoring_warning')}</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="red"
|
||||||
|
onClick={handleStop}
|
||||||
|
ml={2}
|
||||||
|
isLoading={stopMonitoring.isLoading || updateVenue.isLoading}
|
||||||
|
>
|
||||||
|
{t('analytics.stop_monitoring')}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StopMonitoringButton;
|
||||||
@@ -3,15 +3,10 @@ import { Heading } from '@chakra-ui/react';
|
|||||||
import { ActionMeta, InputActionMeta, Select, SingleValue } from 'chakra-react-select';
|
import { ActionMeta, InputActionMeta, Select, SingleValue } from 'chakra-react-select';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const MacSearchBar = (
|
const MacSearchBar: React.FC<{ macs?: string[]; setMac: React.Dispatch<React.SetStateAction<string | undefined>> }> = ({
|
||||||
{
|
macs,
|
||||||
macs,
|
setMac,
|
||||||
setMac
|
}) => {
|
||||||
}: {
|
|
||||||
macs?: string[]
|
|
||||||
setMac: React.Dispatch<React.SetStateAction<string | undefined>>
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
@@ -19,25 +19,14 @@ import {
|
|||||||
import useControlledTable from 'hooks/useControlledTable';
|
import useControlledTable from 'hooks/useControlledTable';
|
||||||
import { Column, SortInfo } from 'models/Table';
|
import { Column, SortInfo } from 'models/Table';
|
||||||
|
|
||||||
const ClientLifecyleTable = (
|
const ClientLifecyleTable: React.FC<{
|
||||||
{
|
venueId: string;
|
||||||
venueId,
|
mac?: string;
|
||||||
mac,
|
fromDate: number;
|
||||||
fromDate,
|
endDate: number;
|
||||||
endDate,
|
timePickers: React.ReactNode;
|
||||||
refreshId,
|
searchBar: React.ReactNode;
|
||||||
timePickers,
|
}> = ({ venueId, mac, fromDate, endDate, timePickers, searchBar }) => {
|
||||||
searchBar
|
|
||||||
}: {
|
|
||||||
venueId: string
|
|
||||||
mac?: string
|
|
||||||
fromDate: number
|
|
||||||
endDate: number
|
|
||||||
refreshId: number
|
|
||||||
timePickers: React.ReactNode
|
|
||||||
searchBar: React.ReactNode
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [sortInfo, setSortInfo] = useState<SortInfo>([{ id: 'timestamp', sort: 'dsc' }]);
|
const [sortInfo, setSortInfo] = useState<SortInfo>([{ id: 'timestamp', sort: 'dsc' }]);
|
||||||
const {
|
const {
|
||||||
@@ -48,8 +37,8 @@ const ClientLifecyleTable = (
|
|||||||
} = useControlledTable({
|
} = useControlledTable({
|
||||||
useCount: useGetClientLifecycleCount as (props: unknown) => UseQueryResult,
|
useCount: useGetClientLifecycleCount as (props: unknown) => UseQueryResult,
|
||||||
useGet: useGetClientLifecycle as (props: unknown) => UseQueryResult,
|
useGet: useGetClientLifecycle as (props: unknown) => UseQueryResult,
|
||||||
countParams: { venueId, mac, sortInfo, fromDate, endDate, refreshId },
|
countParams: { venueId, mac, sortInfo, fromDate, endDate },
|
||||||
getParams: { venueId, mac, sortInfo, fromDate, endDate, refreshId },
|
getParams: { venueId, mac, sortInfo, fromDate, endDate },
|
||||||
});
|
});
|
||||||
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
|
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
|
||||||
const { data: tableSpecs } = useGetClientLifecycleTableSpecs();
|
const { data: tableSpecs } = useGetClientLifecycleTableSpecs();
|
||||||
@@ -66,8 +55,8 @@ const ClientLifecyleTable = (
|
|||||||
const dbCell = useCallback((cell, key) => <DecibelCell db={cell.row.values[key]} key={uuid()} />, []);
|
const dbCell = useCallback((cell, key) => <DecibelCell db={cell.row.values[key]} key={uuid()} />, []);
|
||||||
const numberCell = useCallback((cell, key) => <NumberCell value={cell.row.values[key]} key={uuid()} />, []);
|
const numberCell = useCallback((cell, key) => <NumberCell value={cell.row.values[key]} key={uuid()} />, []);
|
||||||
|
|
||||||
const columns: Column[] = useMemo((): Column[] => {
|
const columns: Column<unknown>[] = useMemo((): Column<unknown>[] => {
|
||||||
const cols: Column[] = [
|
const cols: Column<unknown>[] = [
|
||||||
{
|
{
|
||||||
id: 'timestamp',
|
id: 'timestamp',
|
||||||
Header: t('common.timestamp'),
|
Header: t('common.timestamp'),
|
||||||
@@ -346,14 +335,17 @@ const ClientLifecyleTable = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box my="10px" display="flex">
|
<Box display="flex" pt={2}>
|
||||||
<Box w="300px">{searchBar}</Box>
|
<Box w="200px" ml={2}>
|
||||||
|
{searchBar}
|
||||||
|
</Box>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ColumnPicker
|
<ColumnPicker
|
||||||
columns={columns}
|
columns={columns}
|
||||||
hiddenColumns={hiddenColumns}
|
hiddenColumns={hiddenColumns}
|
||||||
setHiddenColumns={setHiddenColumns}
|
setHiddenColumns={setHiddenColumns}
|
||||||
preference="provisioning.clientLifecycle.hiddenColumns"
|
preference="provisioning.clientLifecycle.hiddenColumns"
|
||||||
|
isCompact
|
||||||
/>
|
/>
|
||||||
{timePickers}
|
{timePickers}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import ClientLifecycleDatePickers from '../DatePickers';
|
||||||
|
import MacSearchBar from './MacSearchBar';
|
||||||
|
import ClientLifecyleTable from './Table';
|
||||||
|
import LoadingOverlay from 'components/LoadingOverlay';
|
||||||
|
import { axiosAnalytics } from 'utils/axiosInstances';
|
||||||
|
import { getHoursAgo } from 'utils/dateFormatting';
|
||||||
|
|
||||||
|
const getPartialClients = async (venueId: string, offset: number) =>
|
||||||
|
axiosAnalytics
|
||||||
|
.get(`wifiClientHistory?macsOnly=true&venue=${venueId}&limit=500&offset=${offset}`)
|
||||||
|
.then(({ data }) => data.entries as string[]);
|
||||||
|
|
||||||
|
const getAllClients = async (venueId: string) => {
|
||||||
|
const allClients: string[] = [];
|
||||||
|
let continueFirmware = true;
|
||||||
|
let offset = 0;
|
||||||
|
while (continueFirmware) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const newClients = await getPartialClients(venueId, offset);
|
||||||
|
if (newClients === null || newClients.length === 0 || newClients.length < 500 || offset >= 50000)
|
||||||
|
continueFirmware = false;
|
||||||
|
allClients.push(...newClients);
|
||||||
|
offset += 500;
|
||||||
|
}
|
||||||
|
return allClients;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
venueId: string;
|
||||||
|
}
|
||||||
|
const VenueClientLifecycle: React.FC<Props> = ({ venueId }) => {
|
||||||
|
const [macs, setMacs] = useState<string[] | undefined>();
|
||||||
|
const [mac, setMac] = useState<string | undefined>();
|
||||||
|
const [time, setTime] = React.useState<{ start: Date; end: Date }>({
|
||||||
|
start: getHoursAgo(5 * 24),
|
||||||
|
end: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChange = (start: Date, end: Date) => {
|
||||||
|
setTime({ start, end });
|
||||||
|
};
|
||||||
|
const onClear = () => {
|
||||||
|
setTime({
|
||||||
|
start: getHoursAgo(5 * 24),
|
||||||
|
end: new Date(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMacs = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const newMacs = await getAllClients(venueId);
|
||||||
|
return newMacs;
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [venueId]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
getMacs().then((res) => setMacs(res));
|
||||||
|
}, [getMacs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingOverlay isLoading={!macs}>
|
||||||
|
<Box minHeight="200px">
|
||||||
|
<ClientLifecyleTable
|
||||||
|
fromDate={Math.floor(time.start.getTime() / 1000)}
|
||||||
|
endDate={Math.floor(time.end.getTime() / 1000)}
|
||||||
|
venueId={venueId}
|
||||||
|
mac={mac}
|
||||||
|
timePickers={
|
||||||
|
<ClientLifecycleDatePickers
|
||||||
|
defaults={{ start: getHoursAgo(5 * 24), end: new Date() }}
|
||||||
|
setTime={onChange}
|
||||||
|
onClear={onClear}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
searchBar={<MacSearchBar macs={macs} setMac={setMac} />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</LoadingOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VenueClientLifecycle;
|
||||||
@@ -57,7 +57,6 @@ const DeviceTypeStat = ({ data, handleModalClick }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
mb={4}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -57,7 +57,6 @@ const FirmwareStat = ({ data, handleModalClick }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
mb={4}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -36,7 +36,6 @@ const HealthStat = ({ data, handleModalClick }) => {
|
|||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
color={getHealthColor()}
|
color={getHealthColor()}
|
||||||
mb={4}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -35,7 +35,6 @@ const MemoryStat = ({ data, handleModalClick }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
mb={4}
|
|
||||||
color={getMemoryColor()}
|
color={getMemoryColor()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user