feat: add the Trace Explorer page with Query Builder (#2843)

* feat: update the SideNav component

* feat: add the Trace Explorer page with Query Builder

* chore: build is fixed

* chore: tsc build is fixed

* chore: menu items is updated

---------

Co-authored-by: Nazarenko19 <danil.nazarenko2000@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
dnazarenkosignoz 2023-06-19 15:57:58 +03:00 committed by GitHub
parent b782bd8909
commit 37fc00b55f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 464 additions and 111 deletions

View File

@ -15,6 +15,11 @@ export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
export const TracesExplorer = Loadable(
() =>
import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'),
);
export const TraceFilter = Loadable(
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
);

View File

@ -30,6 +30,7 @@ import {
StatusPage,
TraceDetail,
TraceFilter,
TracesExplorer,
UnAuthorized,
UsageExplorerPage,
} from './pageComponents';
@ -140,6 +141,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'TRACE',
},
{
path: ROUTES.TRACES_EXPLORER,
exact: true,
component: TracesExplorer,
isPrivate: true,
key: 'TRACES_EXPLORER',
},
{
path: ROUTES.CHANNELS_NEW,
exact: true,

View File

@ -5,6 +5,7 @@ const ROUTES = {
SERVICE_MAP: '/service-map',
TRACE: '/trace',
TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer',
SETTINGS: '/settings',
INSTRUMENTATION: '/get-started',
USAGE_EXPLORER: '/usage-explorer',
@ -31,7 +32,6 @@ const ROUTES = {
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses',
TRACE_EXPLORER: '/trace-explorer',
};
export default ROUTES;

View File

@ -0,0 +1,7 @@
import { CSSProperties } from 'react';
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
export const defaultSelectStyle: CSSProperties = {
minWidth: '6rem',
};

View File

@ -0,0 +1,69 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Select } from 'antd';
import { memo, useMemo } from 'react';
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container } from './styles';
interface ControlsProps {
count: number;
countPerPage: number;
isLoading: boolean;
handleNavigatePrevious: () => void;
handleNavigateNext: () => void;
handleCountItemsPerPageChange: (e: number) => void;
}
function Controls(props: ControlsProps): JSX.Element | null {
const {
count,
isLoading,
countPerPage,
handleNavigatePrevious,
handleNavigateNext,
handleCountItemsPerPageChange,
} = props;
const isNextAndPreviousDisabled = useMemo(
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage,
[isLoading, countPerPage, count],
);
return (
<Container>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigatePrevious}
>
<LeftOutlined /> Previous
</Button>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigateNext}
>
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={countPerPage}
onChange={handleCountItemsPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Container>
);
}
export default memo(Controls);

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
gap: 0.5rem;
`;

View File

@ -1 +0,0 @@
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];

View File

@ -1,16 +1,11 @@
import {
CloudDownloadOutlined,
FastBackwardOutlined,
LeftOutlined,
RightOutlined,
} from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps, Select } from 'antd';
import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import Controls from 'container/Controls';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs from 'dayjs';
import { FlatLogData } from 'lib/logs/flatLogData';
import { defaultSelectStyle } from 'pages/Logs/config';
import * as Papa from 'papaparse';
import { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@ -26,7 +21,6 @@ import {
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container, DownloadLogButton } from './styles';
function LogControls(): JSX.Element | null {
@ -149,15 +143,6 @@ function LogControls(): JSX.Element | null {
const isLoading = isLogsLoading || isLoadingAggregate;
const isNextAndPreviousDisabled = useMemo(
() =>
isLoading ||
logLinesPerPage === 0 ||
logs.length === 0 ||
logs.length < logLinesPerPage,
[isLoading, logLinesPerPage, logs.length],
);
if (liveTail !== 'STOPPED') {
return null;
}
@ -179,37 +164,14 @@ function LogControls(): JSX.Element | null {
<FastBackwardOutlined /> Go to latest
</Button>
<Divider type="vertical" />
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigatePrevious}
>
<LeftOutlined /> Previous
</Button>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigateNext}
>
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={logLinesPerPage}
onChange={handleLogLinesPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
<Controls
isLoading={isLoading}
count={logs.length}
countPerPage={logLinesPerPage}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
/>
</Container>
);
}

View File

@ -32,7 +32,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const flattenLogData: Record<string, any> | null = useMemo(
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
[logData],
);

View File

@ -54,7 +54,7 @@ export const QueryBuilder = memo(function QueryBuilder({
);
return (
<Row gutter={[0, 20]} justify="start">
<Row style={{ width: '100%' }} gutter={[0, 20]} justify="start">
<Col span={24}>
<Row gutter={[0, 50]}>
{currentQuery.builder.queryData.map((query, index) => (

View File

@ -1,4 +1,4 @@
import { Col, Row } from 'antd';
import { Col, Row, Typography } from 'antd';
import { Fragment, memo, ReactNode, useState } from 'react';
// ** Types
@ -46,7 +46,9 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
<Col span={24}>
<StyledInner onClick={handleToggleOpenFilters}>
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
{!isOpenedFilters && (
<Typography>Add conditions for {filtersTexts}</Typography>
)}
</StyledInner>
</Col>
{isOpenedFilters && <Col span={24}>{children}</Col>}

View File

@ -1,3 +1,4 @@
import { Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo } from 'react';
@ -11,5 +12,9 @@ export const FilterLabel = memo(function FilterLabel({
}: FilterLabelProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return <StyledLabel isDarkMode={isDarkMode}>{label}</StyledLabel>;
return (
<StyledLabel isDarkMode={isDarkMode}>
<Typography>{label}</Typography>
</StyledLabel>
);
});

View File

@ -30,10 +30,10 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.TRACE]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
[ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.VERSION]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes],
};

View File

@ -38,35 +38,36 @@ const menus: SidebarMenu[] = [
icon: <BarChartOutlined />,
},
{
key: 'traces',
key: ROUTES.TRACE,
label: 'Traces',
icon: <MenuOutlined />,
children: [
{
key: ROUTES.TRACE,
label: 'Traces',
},
// {
// key: ROUTES.TRACE_EXPLORER,
// label: 'Explorer',
// },
],
// children: [
// {
// key: ROUTES.TRACE,
// label: 'Traces',
// },
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.TRACES_EXPLORER,
// label: "Explorer",
// },
// ],
},
{
key: 'logs',
key: ROUTES.LOGS,
label: 'Logs',
icon: <AlignLeftOutlined />,
children: [
{
key: ROUTES.LOGS,
label: 'Search',
},
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.LOGS_EXPLORER,
// label: 'Views',
// },
],
// children: [
// {
// key: ROUTES.LOGS,
// label: 'Search',
// },
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.LOGS_EXPLORER,
// label: 'Views',
// },
// ],
},
{
key: ROUTES.ALL_DASHBOARD,

View File

@ -5,6 +5,7 @@ import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
const breadcrumbNameMap = {
[ROUTES.APPLICATION]: 'Services',
[ROUTES.TRACE]: 'Traces',
[ROUTES.TRACES_EXPLORER]: 'Traces Explorer',
[ROUTES.SERVICE_MAP]: 'Service Map',
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
[ROUTES.INSTRUMENTATION]: 'Get Started',

View File

@ -0,0 +1,25 @@
import Controls from 'container/Controls';
import { memo } from 'react';
import { Container } from './styles';
function TraceExplorerControls(): JSX.Element | null {
const handleCountItemsPerPageChange = (): void => {};
const handleNavigatePrevious = (): void => {};
const handleNavigateNext = (): void => {};
return (
<Container>
<Controls
isLoading={false}
count={0}
countPerPage={0}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
/>
</Container>
);
}
export default memo(TraceExplorerControls);

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
`;

View File

@ -0,0 +1,32 @@
import { Button } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { ButtonWrapper, Container } from './styles';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
return (
<Container>
<QueryBuilder
panelType={PANEL_TYPES.TIME_SERIES}
config={{
queryVariant: 'static',
initialDataSource: DataSource.TRACES,
}}
actions={
<ButtonWrapper>
<Button onClick={handleRunQuery} type="primary">
Run Query
</Button>
</ButtonWrapper>
}
/>
</Container>
);
}
export default QuerySection;

View File

@ -0,0 +1,16 @@
import { Col } from 'antd';
import Card from 'antd/es/card/Card';
import styled from 'styled-components';
export const Container = styled(Card)`
border: none;
background: inherit;
.ant-card-body {
padding: 0;
}
`;
export const ButtonWrapper = styled(Col)`
margin-left: auto;
`;

View File

@ -0,0 +1,73 @@
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { initialQueriesMap } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import getChartData from 'lib/getChartData';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Container, ErrorText } from './styles';
function TimeSeriesView(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { data, isLoading, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
graphType: 'graph',
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource: 'traces',
},
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
stagedQuery,
maxTime,
minTime,
],
enabled: !!stagedQuery,
},
);
const chartData = useMemo(
() =>
getChartData({
queryData: [
{
queryData: data?.payload?.data?.result || [],
},
],
}),
[data],
);
return (
<Container>
{isLoading && <Spinner height="50vh" size="small" tip="Loading..." />}
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{!isLoading && !isError && (
<Graph
animate={false}
data={chartData}
name="tracesExplorerGraph"
type="line"
/>
)}
</Container>
);
}
export default TimeSeriesView;

View File

@ -0,0 +1,17 @@
import { Typography } from 'antd';
import Card from 'antd/es/card/Card';
import styled from 'styled-components';
export const Container = styled(Card)`
position: relative;
margin: 0.5rem 0 3.1rem 0;
.ant-card-body {
height: 50vh;
min-height: 350px;
}
`;
export const ErrorText = styled(Typography)`
text-align: center;
`;

View File

@ -1,5 +1,4 @@
import GetMinMax from 'lib/getMinMax';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import store from 'store';
export const getDashboardVariables = (): Record<string, unknown> => {
@ -13,16 +12,11 @@ export const getDashboardVariables = (): Record<string, unknown> => {
data: { variables = {} },
} = selectedDashboard;
const minMax = GetMinMax(globalTime.selectedTime, [
globalTime.minTime / 1000000,
globalTime.maxTime / 1000000,
]);
const { start, end } = GetStartAndEndTime({
const { start, end } = getStartEndRangeTime({
type: 'GLOBAL_TIME',
minTime: minMax.minTime,
maxTime: minMax.maxTime,
interval: globalTime.selectedTime,
});
const variablesTuple: Record<string, unknown> = {
SIGNOZ_START_TIME: parseInt(start, 10) * 1e3,
SIGNOZ_END_TIME: parseInt(end, 10) * 1e3,

View File

@ -0,0 +1,48 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import store from 'store';
import getMaxMinTime from './getMaxMinTime';
import getMinMax from './getMinMax';
import getStartAndEndTime from './getStartAndEndTime';
const getStartEndRangeTime = ({
type = 'GLOBAL_TIME',
graphType = null,
interval = 'custom',
}: GetStartEndRangeTimesProps): GetStartEndRangeTimesPayload => {
const { globalTime } = store.getState();
const minMax = getMinMax(interval, [
globalTime.minTime / 1000000,
globalTime.maxTime / 1000000,
]);
const maxMinTime = getMaxMinTime({
graphType,
maxTime: minMax.maxTime,
minTime: minMax.minTime,
});
const { end, start } = getStartAndEndTime({
type,
maxTime: maxMinTime.maxTime,
minTime: maxMinTime.minTime,
});
return { start, end };
};
interface GetStartEndRangeTimesProps {
type?: timePreferenceType;
graphType?: ITEMS | null;
interval?: Time;
}
interface GetStartEndRangeTimesPayload {
start: string;
end: string;
}
export default getStartEndRangeTime;

View File

@ -0,0 +1,6 @@
export const CURRENT_TRACES_EXPLORER_TAB = 'currentTab';
export enum TracesExplorerTabs {
TIME_SERIES = 'times-series',
TRACES = 'traces',
}

View File

@ -0,0 +1,59 @@
import { Tabs } from 'antd';
import { initialQueriesMap } from 'constants/queryBuilder';
import QuerySection from 'container/TracesExplorer/QuerySection';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { CURRENT_TRACES_EXPLORER_TAB, TracesExplorerTabs } from './constants';
import { Container } from './styles';
import { getTabsItems } from './utils';
function TracesExplorer(): JSX.Element {
const urlQuery = useUrlQuery();
const history = useHistory();
const location = useLocation();
const currentUrlTab = urlQuery.get(
CURRENT_TRACES_EXPLORER_TAB,
) as TracesExplorerTabs;
const currentTab = currentUrlTab || TracesExplorerTabs.TIME_SERIES;
const tabsItems = getTabsItems();
const redirectWithCurrentTab = useCallback(
(tabKey: string): void => {
urlQuery.set(CURRENT_TRACES_EXPLORER_TAB, tabKey);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[history, location, urlQuery],
);
const handleTabChange = useCallback(
(tabKey: string): void => {
redirectWithCurrentTab(tabKey);
},
[redirectWithCurrentTab],
);
useShareBuilderUrl({ defaultValue: initialQueriesMap.traces });
useEffect(() => {
if (currentUrlTab) return;
redirectWithCurrentTab(TracesExplorerTabs.TIME_SERIES);
}, [currentUrlTab, redirectWithCurrentTab]);
return (
<>
<QuerySection />
<Container>
<Tabs activeKey={currentTab} items={tabsItems} onChange={handleTabChange} />
</Container>
</>
);
}
export default TracesExplorer;

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
export const Container = styled.div`
margin: 1rem 0;
`;

View File

@ -0,0 +1,17 @@
import { TabsProps } from 'antd';
import TimeSeriesView from 'container/TracesExplorer/TimeSeriesView';
import { TracesExplorerTabs } from './constants';
export const getTabsItems = (): TabsProps['items'] => [
{
label: 'Time Series',
key: TracesExplorerTabs.TIME_SERIES,
children: <TimeSeriesView />,
},
{
label: 'Traces',
key: TracesExplorerTabs.TRACES,
children: <div>Traces tab</div>,
},
];

View File

@ -5,19 +5,16 @@
import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import GetMaxMinTime from 'lib/getMaxMinTime';
import GetMinMax from 'lib/getMinMax';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import getStep from 'lib/getStep';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import store from 'store';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { SuccessResponse } from 'types/api';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EQueryType } from 'types/common/dashboard';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
export async function GetMetricQueryRange({
query,
@ -25,6 +22,7 @@ export async function GetMetricQueryRange({
graphType,
selectedTime,
variables = {},
params = {},
}: GetQueryResultsProps): Promise<SuccessResponse<MetricRangePayloadProps>> {
const queryData = query[query.queryType];
let legendMap: Record<string, string> = {};
@ -83,30 +81,18 @@ export async function GetMetricQueryRange({
return;
}
const { globalTime } = store.getState();
const minMax = GetMinMax(globalSelectedInterval, [
globalTime.minTime / 1000000,
globalTime.maxTime / 1000000,
]);
const getMaxMinTime = GetMaxMinTime({
graphType: null,
maxTime: minMax.maxTime,
minTime: minMax.minTime,
});
const { end, start } = GetStartAndEndTime({
const { start, end } = getStartEndRangeTime({
type: selectedTime,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
interval: globalSelectedInterval,
});
const response = await getMetricsQueryRange({
start: parseInt(start, 10) * 1e3,
end: parseInt(end, 10) * 1e3,
step: getStep({ start, end, inputFormat: 'ms' }),
variables,
...QueryPayload,
...params,
});
if (response.statusCode >= 400) {
throw new Error(
@ -148,4 +134,5 @@ export interface GetQueryResultsProps {
selectedTime: timePreferenceType;
globalSelectedInterval: Time;
variables?: Record<string, unknown>;
params?: Record<string, unknown>;
}

View File

@ -63,6 +63,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
SIGN_UP: ['ADMIN', 'EDITOR', 'VIEWER'],
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
TRACES_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
TRACE: ['ADMIN', 'EDITOR', 'VIEWER'],
TRACE_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
UN_AUTHORIZED: ['ADMIN', 'EDITOR', 'VIEWER'],
@ -71,5 +72,4 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
LIST_LICENSES: ['ADMIN'],
TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
};