mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 14:15:55 +08:00
feat: create live logs page and custom top nav (#3315)
* feat: create live logs page and custom top nav * fix: success button color * fix: turn back color * feat: add live logs where clause (#3325) * feat: add live logs where clause * fix: undefined scenario * feat: get live data (#3337) * feat: get live data * fix: change color, change number format * chore: useMemo is updated * feat: add live logs list (#3341) * feat: add live logs list * feat: hide view if error, clear logs * feat: add condition for disable initial loading * fix: double request * fix: render id in the where clause * fix: render where clause and live list * fix: last log padding * fix: list data loading * fix: no logs text * fix: logs list size * fix: small issues * fix: render view with memo --------- Co-authored-by: Palash Gupta <palashgdev@gmail.com> --------- Co-authored-by: Palash Gupta <palashgdev@gmail.com> --------- Co-authored-by: Palash Gupta <palashgdev@gmail.com> * fix: build is fixed --------- Co-authored-by: Palash Gupta <palashgdev@gmail.com> Co-authored-by: Yunus M <myounis.ar@live.com>
This commit is contained in:
parent
337e33eb8a
commit
d184486978
1
frontend/public/locales/en-GB/logs.json
Normal file
1
frontend/public/locales/en-GB/logs.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{ "fetching_log_lines": "Fetching log lines" }
|
@ -29,6 +29,7 @@
|
|||||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||||
"LOGS": "SigNoz | Logs",
|
"LOGS": "SigNoz | Logs",
|
||||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||||
|
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
|
1
frontend/public/locales/en/logs.json
Normal file
1
frontend/public/locales/en/logs.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{ "fetching_log_lines": "Fetching log lines" }
|
@ -29,6 +29,7 @@
|
|||||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||||
"LOGS": "SigNoz | Logs",
|
"LOGS": "SigNoz | Logs",
|
||||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||||
|
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
|
@ -110,6 +110,10 @@ export const LogsExplorer = Loadable(
|
|||||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
|
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const LiveLogs = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
|
||||||
|
);
|
||||||
|
|
||||||
export const Login = Loadable(
|
export const Login = Loadable(
|
||||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
GettingStarted,
|
GettingStarted,
|
||||||
LicensePage,
|
LicensePage,
|
||||||
ListAllALertsPage,
|
ListAllALertsPage,
|
||||||
|
LiveLogs,
|
||||||
Login,
|
Login,
|
||||||
Logs,
|
Logs,
|
||||||
LogsExplorer,
|
LogsExplorer,
|
||||||
@ -234,6 +235,13 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'LOGS_EXPLORER',
|
key: 'LOGS_EXPLORER',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.LIVE_LOGS,
|
||||||
|
exact: true,
|
||||||
|
component: LiveLogs,
|
||||||
|
key: 'LIVE_LOGS',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.LOGIN,
|
path: ROUTES.LOGIN,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
@ -4,11 +4,11 @@ import { AxiosError } from 'axios';
|
|||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
MetricRangePayloadV3,
|
MetricRangePayloadV3,
|
||||||
MetricsRangeProps,
|
QueryRangePayload,
|
||||||
} from 'types/api/metrics/getQueryRange';
|
} from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
export const getMetricsQueryRange = async (
|
export const getMetricsQueryRange = async (
|
||||||
props: MetricsRangeProps,
|
props: QueryRangePayload,
|
||||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/query_range', props);
|
const response = await axios.post('/query_range', props);
|
||||||
|
5
frontend/src/constants/liveTail.ts
Normal file
5
frontend/src/constants/liveTail.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const LIVE_TAIL_HEARTBEAT_TIMEOUT = 600000;
|
||||||
|
|
||||||
|
export const LIVE_TAIL_GRAPH_INTERVAL = 60000;
|
||||||
|
|
||||||
|
export const MAX_LOGS_LIST_SIZE = 1000;
|
@ -29,6 +29,7 @@ const ROUTES = {
|
|||||||
NOT_FOUND: '/not-found',
|
NOT_FOUND: '/not-found',
|
||||||
LOGS: '/logs',
|
LOGS: '/logs',
|
||||||
LOGS_EXPLORER: '/logs-explorer',
|
LOGS_EXPLORER: '/logs-explorer',
|
||||||
|
LIVE_LOGS: '/logs-explorer/live',
|
||||||
HOME_PAGE: '/',
|
HOME_PAGE: '/',
|
||||||
PASSWORD_RESET: '/password-reset',
|
PASSWORD_RESET: '/password-reset',
|
||||||
LIST_LICENSES: '/licenses',
|
LIST_LICENSES: '/licenses',
|
||||||
|
@ -52,6 +52,8 @@ const themeColors = {
|
|||||||
gamboge: '#D89614',
|
gamboge: '#D89614',
|
||||||
bckgGrey: '#1d1d1d',
|
bckgGrey: '#1d1d1d',
|
||||||
lightBlue: '#177ddc',
|
lightBlue: '#177ddc',
|
||||||
|
buttonSuccessRgb: '73, 170, 25',
|
||||||
|
red: '#E84749',
|
||||||
};
|
};
|
||||||
|
|
||||||
export { themeColors };
|
export { themeColors };
|
||||||
|
35
frontend/src/container/LiveLogs/BackButton/index.tsx
Normal file
35
frontend/src/container/LiveLogs/BackButton/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
function BackButton(): JSX.Element {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { resetQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
const compositeQuery = initialQueriesMap.logs;
|
||||||
|
|
||||||
|
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(compositeQuery));
|
||||||
|
|
||||||
|
const path = `${ROUTES.LOGS_EXPLORER}?${JSONCompositeQuery}`;
|
||||||
|
|
||||||
|
const { queryType, ...queryState } = initialQueriesMap.logs;
|
||||||
|
|
||||||
|
resetQuery(queryState);
|
||||||
|
|
||||||
|
history.push(path);
|
||||||
|
}, [history, resetQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button icon={<ArrowLeftOutlined />} onClick={handleBack}>
|
||||||
|
Exit live view
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BackButton;
|
78
frontend/src/container/LiveLogs/FiltersInput/index.tsx
Normal file
78
frontend/src/container/LiveLogs/FiltersInput/index.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Col } from 'antd';
|
||||||
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useEventSource } from 'providers/EventSource';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
Query,
|
||||||
|
TagFilter,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { getQueryWithoutFilterId } from '../utils';
|
||||||
|
import {
|
||||||
|
ContainerStyled,
|
||||||
|
FilterSearchInputStyled,
|
||||||
|
SearchButtonStyled,
|
||||||
|
} from './styles';
|
||||||
|
|
||||||
|
function FiltersInput(): JSX.Element {
|
||||||
|
const {
|
||||||
|
stagedQuery,
|
||||||
|
handleSetQueryData,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
currentQuery,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const { initialLoading, handleSetInitialLoading } = useEventSource();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(filters: TagFilter) => {
|
||||||
|
const listQueryData = stagedQuery?.builder.queryData[0];
|
||||||
|
|
||||||
|
if (!listQueryData) return;
|
||||||
|
|
||||||
|
const queryData: IBuilderQuery = {
|
||||||
|
...listQueryData,
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetQueryData(0, queryData);
|
||||||
|
},
|
||||||
|
[stagedQuery, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = useMemo(() => {
|
||||||
|
if (stagedQuery && stagedQuery.builder.queryData.length > 0) {
|
||||||
|
return stagedQuery?.builder.queryData[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialQueriesMap.logs.builder.queryData[0];
|
||||||
|
}, [stagedQuery]);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
if (initialLoading) {
|
||||||
|
handleSetInitialLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedQuery: Query = getQueryWithoutFilterId(currentQuery);
|
||||||
|
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
}, [
|
||||||
|
initialLoading,
|
||||||
|
currentQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
handleSetInitialLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContainerStyled>
|
||||||
|
<Col flex={1}>
|
||||||
|
<FilterSearchInputStyled query={query} onChange={handleChange} />
|
||||||
|
</Col>
|
||||||
|
<SearchButtonStyled onSearch={handleSearch} />
|
||||||
|
</ContainerStyled>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiltersInput;
|
24
frontend/src/container/LiveLogs/FiltersInput/styles.ts
Normal file
24
frontend/src/container/LiveLogs/FiltersInput/styles.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Input, Row } from 'antd';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const FilterSearchInputStyled = styled(QueryBuilderSearch)`
|
||||||
|
z-index: 1;
|
||||||
|
.ant-select-selector {
|
||||||
|
width: 100%;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ContainerStyled = styled(Row)`
|
||||||
|
color: ${themeColors.white};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SearchButtonStyled = styled(Input.Search)`
|
||||||
|
width: 2rem;
|
||||||
|
.ant-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
58
frontend/src/container/LiveLogs/ListViewPanel/index.tsx
Normal file
58
frontend/src/container/LiveLogs/ListViewPanel/index.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Button, Popover, Select, Space } from 'antd';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
|
import {
|
||||||
|
defaultSelectStyle,
|
||||||
|
logsOptions,
|
||||||
|
viewModeOptionList,
|
||||||
|
} from 'pages/Logs/config';
|
||||||
|
import PopoverContent from 'pages/Logs/PopoverContent';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
function ListViewPanel(): JSX.Element {
|
||||||
|
const { config } = useOptionsMenu({
|
||||||
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateOperator: StringOperators.NOOP,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFormatButtonVisible = logsOptions.includes(config.format?.value);
|
||||||
|
|
||||||
|
const renderPopoverContent = useCallback(() => {
|
||||||
|
if (!config.maxLines) return null;
|
||||||
|
const linedPerRow = config.maxLines.value as number;
|
||||||
|
const handleLinesPerRowChange = config.maxLines.onChange as (
|
||||||
|
value: unknown,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverContent
|
||||||
|
linesPerRow={linedPerRow}
|
||||||
|
handleLinesPerRowChange={handleLinesPerRowChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space align="baseline" direction="horizontal">
|
||||||
|
<Select
|
||||||
|
style={defaultSelectStyle}
|
||||||
|
value={config.format?.value}
|
||||||
|
onChange={config.format?.onChange}
|
||||||
|
>
|
||||||
|
{viewModeOptionList.map((option) => (
|
||||||
|
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{isFormatButtonVisible && (
|
||||||
|
<Popover placement="right" content={renderPopoverContent}>
|
||||||
|
<Button>Format</Button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListViewPanel;
|
167
frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx
Normal file
167
frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { Col } from 'antd';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import GoToTop from 'container/GoToTop';
|
||||||
|
import FiltersInput from 'container/LiveLogs/FiltersInput';
|
||||||
|
import LiveLogsTopNav from 'container/LiveLogsTopNav';
|
||||||
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
|
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { useEventSource } from 'providers/EventSource';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { prepareQueryRangePayload } from 'store/actions/dashboard/prepareQueryRangePayload';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { idObject } from '../constants';
|
||||||
|
import ListViewPanel from '../ListViewPanel';
|
||||||
|
import LiveLogsList from '../LiveLogsList';
|
||||||
|
import { prepareQueryByFilter } from '../utils';
|
||||||
|
import { ContentWrapper, LiveLogsChart, Wrapper } from './styles';
|
||||||
|
|
||||||
|
function LiveLogsContainer(): JSX.Element {
|
||||||
|
const [logs, setLogs] = useState<ILog[]>([]);
|
||||||
|
|
||||||
|
const { stagedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const batchedEventsRef = useRef<ILog[]>([]);
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleStartOpenConnection,
|
||||||
|
handleCloseConnection,
|
||||||
|
initialLoading,
|
||||||
|
} = useEventSource();
|
||||||
|
|
||||||
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
|
|
||||||
|
const updateLogs = useCallback(() => {
|
||||||
|
const reversedData = batchedEventsRef.current.reverse();
|
||||||
|
setLogs((prevState) =>
|
||||||
|
[...reversedData, ...prevState].slice(0, MAX_LOGS_LIST_SIZE),
|
||||||
|
);
|
||||||
|
|
||||||
|
batchedEventsRef.current = [];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const debouncedUpdateLogs = useDebouncedFn(updateLogs, 500);
|
||||||
|
|
||||||
|
const batchLiveLog = useCallback(
|
||||||
|
(log: ILog): void => {
|
||||||
|
batchedEventsRef.current.push(log);
|
||||||
|
|
||||||
|
debouncedUpdateLogs();
|
||||||
|
},
|
||||||
|
[debouncedUpdateLogs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGetLiveLogs = useCallback(
|
||||||
|
(event: MessageEvent<string>) => {
|
||||||
|
const data: ILog = JSON.parse(event.data);
|
||||||
|
|
||||||
|
batchLiveLog(data);
|
||||||
|
},
|
||||||
|
[batchLiveLog],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleError = useCallback(() => {
|
||||||
|
notifications.error({ message: 'Sorry, something went wrong' });
|
||||||
|
}, [notifications]);
|
||||||
|
|
||||||
|
useEventSourceEvent('message', handleGetLiveLogs);
|
||||||
|
useEventSourceEvent('error', handleError);
|
||||||
|
|
||||||
|
const getPreparedQuery = useCallback(
|
||||||
|
(query: Query): Query => {
|
||||||
|
const firstLogId: string | null = logs.length ? logs[0].id : null;
|
||||||
|
|
||||||
|
const preparedQuery: Query = prepareQueryByFilter(
|
||||||
|
query,
|
||||||
|
idObject,
|
||||||
|
firstLogId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return preparedQuery;
|
||||||
|
},
|
||||||
|
[logs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStartNewConnection = useCallback(() => {
|
||||||
|
if (!compositeQuery) return;
|
||||||
|
|
||||||
|
handleCloseConnection();
|
||||||
|
|
||||||
|
const preparedQuery = getPreparedQuery(compositeQuery);
|
||||||
|
|
||||||
|
const { queryPayload } = prepareQueryRangePayload({
|
||||||
|
query: preparedQuery,
|
||||||
|
graphType: PANEL_TYPES.LIST,
|
||||||
|
selectedTime: 'GLOBAL_TIME',
|
||||||
|
globalSelectedInterval: globalSelectedTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const encodedQueryPayload = encodeURIComponent(JSON.stringify(queryPayload));
|
||||||
|
|
||||||
|
const queryString = `q=${encodedQueryPayload}`;
|
||||||
|
|
||||||
|
handleStartOpenConnection({ queryString });
|
||||||
|
}, [
|
||||||
|
compositeQuery,
|
||||||
|
globalSelectedTime,
|
||||||
|
getPreparedQuery,
|
||||||
|
handleCloseConnection,
|
||||||
|
handleStartOpenConnection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!compositeQuery || !stagedQuery) return;
|
||||||
|
|
||||||
|
if (compositeQuery.id !== stagedQuery.id || initialLoading) {
|
||||||
|
handleStartNewConnection();
|
||||||
|
}
|
||||||
|
}, [stagedQuery, initialLoading, compositeQuery, handleStartNewConnection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<LiveLogsTopNav />
|
||||||
|
<ContentWrapper gutter={[0, 20]} style={{ color: themeColors.lightWhite }}>
|
||||||
|
<Col span={24}>
|
||||||
|
<FiltersInput />
|
||||||
|
</Col>
|
||||||
|
{initialLoading ? (
|
||||||
|
<Col span={24}>
|
||||||
|
<Spinner style={{ height: 'auto' }} tip="Fetching Logs" />
|
||||||
|
</Col>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Col span={24}>
|
||||||
|
<LiveLogsChart />
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<ListViewPanel />
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<LiveLogsList logs={logs} />
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<GoToTop />
|
||||||
|
</ContentWrapper>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LiveLogsContainer;
|
17
frontend/src/container/LiveLogs/LiveLogsContainer/styles.ts
Normal file
17
frontend/src/container/LiveLogs/LiveLogsContainer/styles.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Row } from 'antd';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import LiveLogsListChart from '../LiveLogsListChart';
|
||||||
|
|
||||||
|
export const LiveLogsChart = styled(LiveLogsListChart)`
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ContentWrapper = styled(Row)`
|
||||||
|
color: rgba(${(themeColors.white, 0.85)});
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Wrapper = styled.div`
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
`;
|
131
frontend/src/container/LiveLogs/LiveLogsList/index.tsx
Normal file
131
frontend/src/container/LiveLogs/LiveLogsList/index.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { Card, Typography } from 'antd';
|
||||||
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
|
||||||
|
import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
|
||||||
|
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
|
||||||
|
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||||
|
import { Heading } from 'container/LogsTable/styles';
|
||||||
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
|
import { contentStyle } from 'container/Trace/Search/config';
|
||||||
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
|
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||||
|
import { useEventSource } from 'providers/EventSource';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
// interfaces
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { LiveLogsListProps } from './types';
|
||||||
|
|
||||||
|
function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||||
|
const ref = useRef<VirtuosoHandle>(null);
|
||||||
|
|
||||||
|
const { t } = useTranslation(['logs']);
|
||||||
|
|
||||||
|
const { isConnectionError, isConnectionLoading } = useEventSource();
|
||||||
|
|
||||||
|
const { activeLogId } = useCopyLogLink();
|
||||||
|
|
||||||
|
const { options } = useOptionsMenu({
|
||||||
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateOperator: StringOperators.NOOP,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLogIndex = useMemo(
|
||||||
|
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||||
|
[logs, activeLogId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useFontFaceObserver(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
family: 'Fira Code',
|
||||||
|
weight: '300',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options.format === 'raw',
|
||||||
|
{
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||||
|
|
||||||
|
const getItemContent = useCallback(
|
||||||
|
(_: number, log: ILog): JSX.Element => {
|
||||||
|
if (options.format === 'raw') {
|
||||||
|
return (
|
||||||
|
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListLogView key={log.id} logData={log} selectedFields={selectedFields} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[options.format, options.maxLines, selectedFields],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeLogId || activeLogIndex < 0) return;
|
||||||
|
|
||||||
|
ref?.current?.scrollToIndex({
|
||||||
|
index: activeLogIndex,
|
||||||
|
align: 'start',
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, [activeLogId, activeLogIndex]);
|
||||||
|
|
||||||
|
const isLoadingList = isConnectionLoading && logs.length === 0;
|
||||||
|
|
||||||
|
if (isLoadingList) {
|
||||||
|
return <Spinner style={{ height: 'auto' }} tip="Fetching Logs" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options.format !== OptionFormatTypes.TABLE && (
|
||||||
|
<Heading>
|
||||||
|
<Typography.Text>Event</Typography.Text>
|
||||||
|
</Heading>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{logs.length === 0 && <Typography>{t('fetching_log_lines')}</Typography>}
|
||||||
|
|
||||||
|
{!isConnectionError && logs.length !== 0 && (
|
||||||
|
<InfinityWrapperStyled>
|
||||||
|
{options.format === 'table' ? (
|
||||||
|
<InfinityTableView
|
||||||
|
ref={ref}
|
||||||
|
isLoading={false}
|
||||||
|
tableViewProps={{
|
||||||
|
logs,
|
||||||
|
fields: selectedFields,
|
||||||
|
linesPerRow: options.maxLines,
|
||||||
|
appendTo: 'end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
|
||||||
|
<Virtuoso
|
||||||
|
ref={ref}
|
||||||
|
useWindowScroll
|
||||||
|
data={logs}
|
||||||
|
totalCount={logs.length}
|
||||||
|
itemContent={getItemContent}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</InfinityWrapperStyled>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(LiveLogsList);
|
5
frontend/src/container/LiveLogs/LiveLogsList/types.ts
Normal file
5
frontend/src/container/LiveLogs/LiveLogsList/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
export type LiveLogsListProps = {
|
||||||
|
logs: ILog[];
|
||||||
|
};
|
70
frontend/src/container/LiveLogs/LiveLogsListChart/index.tsx
Normal file
70
frontend/src/container/LiveLogs/LiveLogsListChart/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { LIVE_TAIL_GRAPH_INTERVAL } from 'constants/liveTail';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||||
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useEventSource } from 'providers/EventSource';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { LiveLogsListChartProps } from './types';
|
||||||
|
|
||||||
|
function LiveLogsListChart({ className }: LiveLogsListChartProps): JSX.Element {
|
||||||
|
const { stagedQuery } = useQueryBuilder();
|
||||||
|
const { isConnectionOpen, isConnectionLoading } = useEventSource();
|
||||||
|
|
||||||
|
const listChartQuery: Query | null = useMemo(() => {
|
||||||
|
if (!stagedQuery) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...stagedQuery,
|
||||||
|
builder: {
|
||||||
|
...stagedQuery.builder,
|
||||||
|
queryData: stagedQuery.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
disabled: false,
|
||||||
|
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: item.filters.items.filter((item) => item.key?.key !== 'id'),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [stagedQuery]);
|
||||||
|
|
||||||
|
const { data, isFetching } = useGetExplorerQueryRange(
|
||||||
|
listChartQuery,
|
||||||
|
PANEL_TYPES.TIME_SERIES,
|
||||||
|
{
|
||||||
|
enabled: isConnectionOpen,
|
||||||
|
refetchInterval: LIVE_TAIL_GRAPH_INTERVAL,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
{ dataSource: DataSource.LOGS },
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData: QueryData[] = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
return data.payload.data.result;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const isLoading = useMemo(
|
||||||
|
() => isFetching || (isConnectionLoading && !isConnectionOpen),
|
||||||
|
[isConnectionLoading, isConnectionOpen, isFetching],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogsExplorerChart
|
||||||
|
isLoading={isLoading}
|
||||||
|
data={chartData}
|
||||||
|
isLabelEnabled={false}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LiveLogsListChart;
|
@ -0,0 +1,3 @@
|
|||||||
|
export type LiveLogsListChartProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
31
frontend/src/container/LiveLogs/constants.ts
Normal file
31
frontend/src/container/LiveLogs/constants.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
initialQueriesMap,
|
||||||
|
initialQueryBuilderFormValuesMap,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
|
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export const liveLogsCompositeQuery: Query = {
|
||||||
|
...initialQueriesMap.logs,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.logs.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueryBuilderFormValuesMap.logs,
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
disabled: true,
|
||||||
|
pageSize: 10,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: FILTERS.DESC }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const idObject: BaseAutocompleteData = {
|
||||||
|
key: 'id',
|
||||||
|
type: '',
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: true,
|
||||||
|
};
|
71
frontend/src/container/LiveLogs/utils.ts
Normal file
71
frontend/src/container/LiveLogs/utils.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
Query,
|
||||||
|
TagFilter,
|
||||||
|
TagFilterItem,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
const getIdFilter = (filtersItems: TagFilterItem[]): TagFilterItem | null =>
|
||||||
|
filtersItems.find((item) => item.key?.key === 'id') || null;
|
||||||
|
|
||||||
|
const getFilter = (
|
||||||
|
filters: TagFilter,
|
||||||
|
tagFilter: BaseAutocompleteData,
|
||||||
|
value: string,
|
||||||
|
): TagFilter => {
|
||||||
|
let newItems = filters.items;
|
||||||
|
|
||||||
|
const isExistIdFilter = getIdFilter(newItems);
|
||||||
|
|
||||||
|
if (isExistIdFilter) {
|
||||||
|
newItems = newItems.map((item) =>
|
||||||
|
item.key?.key === 'id' ? { ...item, value } : item,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newItems = [
|
||||||
|
...newItems,
|
||||||
|
{ value, key: tagFilter, op: OPERATORS['>'], id: uuid() },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items: newItems, op: filters.op };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareQueryByFilter = (
|
||||||
|
query: Query,
|
||||||
|
tagFilter: BaseAutocompleteData,
|
||||||
|
value: string | null,
|
||||||
|
): Query => {
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...query,
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: query.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
filters: value ? getFilter(item.filters, tagFilter, value) : item.filters,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return preparedQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueryWithoutFilterId = (query: Query): Query => {
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...query,
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: query.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: item.filters.items.filter((item) => item.key?.key !== 'id'),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return preparedQuery;
|
||||||
|
};
|
71
frontend/src/container/LiveLogsTopNav/index.tsx
Normal file
71
frontend/src/container/LiveLogsTopNav/index.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import BackButton from 'container/LiveLogs/BackButton';
|
||||||
|
import { getQueryWithoutFilterId } from 'container/LiveLogs/utils';
|
||||||
|
import LocalTopNav from 'container/LocalTopNav';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useEventSource } from 'providers/EventSource';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { LiveButtonStyled } from './styles';
|
||||||
|
|
||||||
|
function LiveLogsTopNav(): JSX.Element {
|
||||||
|
const {
|
||||||
|
isConnectionOpen,
|
||||||
|
isConnectionLoading,
|
||||||
|
initialLoading,
|
||||||
|
handleCloseConnection,
|
||||||
|
handleSetInitialLoading,
|
||||||
|
} = useEventSource();
|
||||||
|
|
||||||
|
const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const isPlaying = isConnectionOpen || isConnectionLoading || initialLoading;
|
||||||
|
|
||||||
|
const onLiveButtonClick = useCallback(() => {
|
||||||
|
if (initialLoading) {
|
||||||
|
handleSetInitialLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!isConnectionOpen && isConnectionLoading) || isConnectionOpen) {
|
||||||
|
handleCloseConnection();
|
||||||
|
} else {
|
||||||
|
const preparedQuery = getQueryWithoutFilterId(currentQuery);
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
initialLoading,
|
||||||
|
isConnectionOpen,
|
||||||
|
isConnectionLoading,
|
||||||
|
currentQuery,
|
||||||
|
handleSetInitialLoading,
|
||||||
|
handleCloseConnection,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const liveButton = useMemo(
|
||||||
|
() => (
|
||||||
|
<Space size={16}>
|
||||||
|
<LiveButtonStyled
|
||||||
|
icon={isPlaying ? <PauseCircleFilled /> : <PlayCircleFilled />}
|
||||||
|
danger={isPlaying}
|
||||||
|
onClick={onLiveButtonClick}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
{isPlaying ? 'Pause' : 'Resume'}
|
||||||
|
</LiveButtonStyled>
|
||||||
|
<BackButton />
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
[isPlaying, onLiveButtonClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocalTopNav
|
||||||
|
actions={liveButton}
|
||||||
|
renderPermissions={{ isDateTimeEnabled: false }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(LiveLogsTopNav);
|
20
frontend/src/container/LiveLogsTopNav/styles.ts
Normal file
20
frontend/src/container/LiveLogsTopNav/styles.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Button, ButtonProps } from 'antd';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||||
|
|
||||||
|
export const LiveButtonStyled = styled(Button)<ButtonProps>`
|
||||||
|
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
|
||||||
|
|
||||||
|
${({ danger }): FlattenSimpleInterpolation =>
|
||||||
|
!danger
|
||||||
|
? css`
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: css``}
|
||||||
|
`;
|
5
frontend/src/container/LiveLogsTopNav/types.ts
Normal file
5
frontend/src/container/LiveLogsTopNav/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type LiveLogsTopNavProps = {
|
||||||
|
getPreparedQuery: (query: Query) => Query;
|
||||||
|
};
|
34
frontend/src/container/LocalTopNav/index.tsx
Normal file
34
frontend/src/container/LocalTopNav/index.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Col, Row, Space } from 'antd';
|
||||||
|
|
||||||
|
import ShowBreadcrumbs from '../TopNav/Breadcrumbs';
|
||||||
|
import DateTimeSelector from '../TopNav/DateTimeSelection';
|
||||||
|
import { Container } from './styles';
|
||||||
|
import { LocalTopNavProps } from './types';
|
||||||
|
|
||||||
|
function LocalTopNav({
|
||||||
|
actions,
|
||||||
|
renderPermissions,
|
||||||
|
}: LocalTopNavProps): JSX.Element | null {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Col span={16}>
|
||||||
|
<ShowBreadcrumbs />
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={8}>
|
||||||
|
<Row justify="end">
|
||||||
|
<Space align="center" size={30} direction="horizontal">
|
||||||
|
{actions}
|
||||||
|
{renderPermissions?.isDateTimeEnabled && (
|
||||||
|
<div>
|
||||||
|
<DateTimeSelector />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LocalTopNav;
|
9
frontend/src/container/LocalTopNav/styles.ts
Normal file
9
frontend/src/container/LocalTopNav/styles.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Row } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled(Row)`
|
||||||
|
&&& {
|
||||||
|
margin-top: 2rem;
|
||||||
|
min-height: 8vh;
|
||||||
|
}
|
||||||
|
`;
|
6
frontend/src/container/LocalTopNav/types.ts
Normal file
6
frontend/src/container/LocalTopNav/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type LocalTopNavProps = {
|
||||||
|
actions?: ReactNode;
|
||||||
|
renderPermissions?: { isDateTimeEnabled: boolean };
|
||||||
|
};
|
@ -3,4 +3,6 @@ import { QueryData } from 'types/api/widgets/getQuery';
|
|||||||
export type LogsExplorerChartProps = {
|
export type LogsExplorerChartProps = {
|
||||||
data: QueryData[];
|
data: QueryData[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isLabelEnabled?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import Graph from 'components/Graph';
|
import Graph from 'components/Graph';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
import getChartData, { GetChartDataProps } from 'lib/getChartData';
|
import getChartData, { GetChartDataProps } from 'lib/getChartData';
|
||||||
import { colors } from 'lib/getRandomColor';
|
import { colors } from 'lib/getRandomColor';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||||
import { CardStyled } from './LogsExplorerChart.styled';
|
import { CardStyled } from './LogsExplorerChart.styled';
|
||||||
@ -10,17 +11,22 @@ import { CardStyled } from './LogsExplorerChart.styled';
|
|||||||
function LogsExplorerChart({
|
function LogsExplorerChart({
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isLabelEnabled = true,
|
||||||
|
className,
|
||||||
}: LogsExplorerChartProps): JSX.Element {
|
}: LogsExplorerChartProps): JSX.Element {
|
||||||
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = (
|
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = useCallback(
|
||||||
element,
|
(element, index, allLabels) => ({
|
||||||
index,
|
|
||||||
allLabels,
|
|
||||||
) => ({
|
|
||||||
label: allLabels[index],
|
|
||||||
data: element,
|
data: element,
|
||||||
backgroundColor: colors[index % colors.length] || 'red',
|
backgroundColor: colors[index % colors.length] || themeColors.red,
|
||||||
borderColor: colors[index % colors.length] || 'red',
|
borderColor: colors[index % colors.length] || themeColors.red,
|
||||||
});
|
...(isLabelEnabled
|
||||||
|
? {
|
||||||
|
label: allLabels[index],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}),
|
||||||
|
[isLabelEnabled],
|
||||||
|
);
|
||||||
|
|
||||||
const graphData = useMemo(
|
const graphData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -32,11 +38,11 @@ function LogsExplorerChart({
|
|||||||
],
|
],
|
||||||
createDataset: handleCreateDatasets,
|
createDataset: handleCreateDatasets,
|
||||||
}),
|
}),
|
||||||
[data],
|
[data, handleCreateDatasets],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardStyled>
|
<CardStyled className={className}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Spinner size="default" height="100%" />
|
<Spinner size="default" height="100%" />
|
||||||
) : (
|
) : (
|
||||||
|
@ -10,6 +10,7 @@ import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
|||||||
import {
|
import {
|
||||||
cloneElement,
|
cloneElement,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
memo,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -67,7 +68,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
const { onEndReached } = infitiyTableProps;
|
|
||||||
const { dataSource, columns } = useTableView({
|
const { dataSource, columns } = useTableView({
|
||||||
...tableViewProps,
|
...tableViewProps,
|
||||||
onClickExpand: onSetActiveLog,
|
onClickExpand: onSetActiveLog,
|
||||||
@ -158,8 +158,11 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
}}
|
}}
|
||||||
itemContent={itemContent}
|
itemContent={itemContent}
|
||||||
fixedHeaderContent={tableHeader}
|
fixedHeaderContent={tableHeader}
|
||||||
endReached={onEndReached}
|
|
||||||
totalCount={dataSource.length}
|
totalCount={dataSource.length}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...(infitiyTableProps?.onEndReached
|
||||||
|
? { endReached: infitiyTableProps.onEndReached }
|
||||||
|
: {})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeContextLog && (
|
{activeContextLog && (
|
||||||
@ -179,4 +182,4 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default InfinityTable;
|
export default memo(InfinityTable);
|
||||||
|
@ -3,7 +3,7 @@ import { UseTableViewProps } from 'components/Logs/TableView/types';
|
|||||||
export type InfinityTableProps = {
|
export type InfinityTableProps = {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
|
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
|
||||||
infitiyTableProps: {
|
infitiyTableProps?: {
|
||||||
onEndReached: (index: number) => void;
|
onEndReached: (index: number) => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,7 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
TagFilter,
|
TagFilter,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
|
|
||||||
const modifiedQueryData: IBuilderQuery = {
|
const modifiedQueryData: IBuilderQuery = {
|
||||||
...listQuery,
|
...listQuery,
|
||||||
aggregateOperator: StringOperators.COUNT,
|
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifiedQuery: Query = {
|
const modifiedQuery: Query = {
|
||||||
|
43
frontend/src/container/LogsTopNav/index.tsx
Normal file
43
frontend/src/container/LogsTopNav/index.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { PlayCircleFilled } from '@ant-design/icons';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { liveLogsCompositeQuery } from 'container/LiveLogs/constants';
|
||||||
|
import LocalTopNav from 'container/LocalTopNav';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { LiveButtonStyled } from './styles';
|
||||||
|
|
||||||
|
function LogsTopNav(): JSX.Element {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleGoLive = useCallback(() => {
|
||||||
|
const JSONCompositeQuery = encodeURIComponent(
|
||||||
|
JSON.stringify(liveLogsCompositeQuery),
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = `${ROUTES.LIVE_LOGS}?${JSONCompositeQuery}`;
|
||||||
|
|
||||||
|
history.push(path);
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
const liveButton = useMemo(
|
||||||
|
() => (
|
||||||
|
<LiveButtonStyled
|
||||||
|
icon={<PlayCircleFilled />}
|
||||||
|
onClick={handleGoLive}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
Go Live
|
||||||
|
</LiveButtonStyled>
|
||||||
|
),
|
||||||
|
[handleGoLive],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<LocalTopNav
|
||||||
|
actions={liveButton}
|
||||||
|
renderPermissions={{ isDateTimeEnabled: true }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogsTopNav;
|
20
frontend/src/container/LogsTopNav/styles.ts
Normal file
20
frontend/src/container/LogsTopNav/styles.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Button, ButtonProps } from 'antd';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||||
|
|
||||||
|
export const LiveButtonStyled = styled(Button)<ButtonProps>`
|
||||||
|
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
|
||||||
|
|
||||||
|
${({ danger }): FlattenSimpleInterpolation =>
|
||||||
|
!danger
|
||||||
|
? css`
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: css``}
|
||||||
|
`;
|
@ -1,3 +1,5 @@
|
|||||||
|
import { RadioChangeEvent } from 'antd';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FieldTitle } from '../styles';
|
import { FieldTitle } from '../styles';
|
||||||
@ -7,6 +9,15 @@ import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
|||||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||||
const { t } = useTranslation(['trace']);
|
const { t } = useTranslation(['trace']);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(event: RadioChangeEvent) => {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
config.onChange(event.target.value);
|
||||||
|
},
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
|
||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -16,7 +27,7 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
|||||||
size="small"
|
size="small"
|
||||||
buttonStyle="solid"
|
buttonStyle="solid"
|
||||||
value={config.value}
|
value={config.value}
|
||||||
onChange={config.onChange}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<RadioButton value="raw">{t('options_menu.raw')}</RadioButton>
|
<RadioButton value="raw">{t('options_menu.raw')}</RadioButton>
|
||||||
<RadioButton value="list">{t('options_menu.default')}</RadioButton>
|
<RadioButton value="list">{t('options_menu.default')}</RadioButton>
|
||||||
|
@ -14,7 +14,9 @@ export interface InitialOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OptionsMenuConfig = {
|
export type OptionsMenuConfig = {
|
||||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
format?: Pick<RadioProps, 'value'> & {
|
||||||
|
onChange: (value: LogViewMode) => void;
|
||||||
|
};
|
||||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||||
addColumn?: Pick<
|
addColumn?: Pick<
|
||||||
SelectProps,
|
SelectProps,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { RadioChangeEvent } from 'antd';
|
|
||||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { LogViewMode } from 'container/LogsTable';
|
||||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -213,10 +213,10 @@ const useOptionsMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleFormatChange = useCallback(
|
const handleFormatChange = useCallback(
|
||||||
(event: RadioChangeEvent) => {
|
(value: LogViewMode) => {
|
||||||
const optionsData: OptionsQuery = {
|
const optionsData: OptionsQuery = {
|
||||||
...optionsQueryData,
|
...optionsQueryData,
|
||||||
format: event.target.value,
|
format: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRedirectWithOptionsData(optionsData);
|
handleRedirectWithOptionsData(optionsData);
|
||||||
|
@ -35,6 +35,7 @@ function QueryBuilderSearch({
|
|||||||
query,
|
query,
|
||||||
onChange,
|
onChange,
|
||||||
whereClauseConfig,
|
whereClauseConfig,
|
||||||
|
className,
|
||||||
}: QueryBuilderSearchProps): JSX.Element {
|
}: QueryBuilderSearchProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
updateTag,
|
updateTag,
|
||||||
@ -163,6 +164,7 @@ function QueryBuilderSearch({
|
|||||||
placeholder={PLACEHOLDER}
|
placeholder={PLACEHOLDER}
|
||||||
value={queryTags}
|
value={queryTags}
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
|
className={className}
|
||||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||||
style={selectStyle}
|
style={selectStyle}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
@ -186,10 +188,12 @@ interface QueryBuilderSearchProps {
|
|||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
onChange: (value: TagFilter) => void;
|
onChange: (value: TagFilter) => void;
|
||||||
whereClauseConfig?: WhereClauseConfig;
|
whereClauseConfig?: WhereClauseConfig;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilderSearch.defaultProps = {
|
QueryBuilderSearch.defaultProps = {
|
||||||
whereClauseConfig: undefined,
|
whereClauseConfig: undefined,
|
||||||
|
className: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CustomTagProps {
|
export interface CustomTagProps {
|
||||||
|
@ -21,6 +21,7 @@ const breadcrumbNameMap = {
|
|||||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||||
[ROUTES.LOGS]: 'Logs',
|
[ROUTES.LOGS]: 'Logs',
|
||||||
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
|
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
|
||||||
|
[ROUTES.LIVE_LOGS]: 'Live View',
|
||||||
[ROUTES.PIPELINES]: 'Pipelines',
|
[ROUTES.PIPELINES]: 'Pipelines',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,3 +84,5 @@ export const routesToSkip = [
|
|||||||
ROUTES.LIST_ALL_ALERT,
|
ROUTES.LIST_ALL_ALERT,
|
||||||
ROUTES.PIPELINES,
|
ROUTES.PIPELINES,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||||
|
@ -3,10 +3,10 @@ import ROUTES from 'constants/routes';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { matchPath, useHistory } from 'react-router-dom';
|
import { matchPath, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import NewExplorerCTA from '../NewExplorerCTA';
|
||||||
import ShowBreadcrumbs from './Breadcrumbs';
|
import ShowBreadcrumbs from './Breadcrumbs';
|
||||||
import DateTimeSelector from './DateTimeSelection';
|
import DateTimeSelector from './DateTimeSelection';
|
||||||
import { routesToSkip } from './DateTimeSelection/config';
|
import { routesToDisable, routesToSkip } from './DateTimeSelection/config';
|
||||||
import NewExplorerCTA from './NewExplorerCTA';
|
|
||||||
import { Container } from './styles';
|
import { Container } from './styles';
|
||||||
|
|
||||||
function TopNav(): JSX.Element | null {
|
function TopNav(): JSX.Element | null {
|
||||||
@ -20,12 +20,20 @@ function TopNav(): JSX.Element | null {
|
|||||||
[location.pathname],
|
[location.pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isDisabled = useMemo(
|
||||||
|
() =>
|
||||||
|
routesToDisable.some((route) =>
|
||||||
|
matchPath(location.pathname, { path: route, exact: true }),
|
||||||
|
),
|
||||||
|
[location.pathname],
|
||||||
|
);
|
||||||
|
|
||||||
const isSignUpPage = useMemo(
|
const isSignUpPage = useMemo(
|
||||||
() => matchPath(location.pathname, { path: ROUTES.SIGN_UP, exact: true }),
|
() => matchPath(location.pathname, { path: ROUTES.SIGN_UP, exact: true }),
|
||||||
[location.pathname],
|
[location.pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSignUpPage) {
|
if (isSignUpPage || isDisabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +48,6 @@ function TopNav(): JSX.Element | null {
|
|||||||
<Row justify="end">
|
<Row justify="end">
|
||||||
<Space align="start" size={60} direction="horizontal">
|
<Space align="start" size={60} direction="horizontal">
|
||||||
<NewExplorerCTA />
|
<NewExplorerCTA />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<DateTimeSelector />
|
<DateTimeSelector />
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import { useQueryBuilder } from './useQueryBuilder';
|
|||||||
export type UseShareBuilderUrlParams = { defaultValue: Query };
|
export type UseShareBuilderUrlParams = { defaultValue: Query };
|
||||||
|
|
||||||
export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||||
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
|
const { redirectWithQueryBuilderData, resetQuery } = useQueryBuilder();
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
const compositeQuery = useGetCompositeQueryParam();
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
@ -21,8 +21,8 @@ export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => (): void => {
|
() => (): void => {
|
||||||
resetStagedQuery();
|
resetQuery();
|
||||||
},
|
},
|
||||||
[resetStagedQuery],
|
[resetQuery],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,20 +2,29 @@ import { EventListener, EventSourceEventMap } from 'event-source-polyfill';
|
|||||||
import { useEventSource } from 'providers/EventSource';
|
import { useEventSource } from 'providers/EventSource';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const useEventSourceEvent = (
|
type EventMap = {
|
||||||
eventName: keyof EventSourceEventMap,
|
message: MessageEvent;
|
||||||
listener: EventListener,
|
open: Event;
|
||||||
|
error: Event;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEventSourceEvent = <T extends keyof EventSourceEventMap>(
|
||||||
|
eventName: T,
|
||||||
|
listener: (event: EventMap[T]) => void,
|
||||||
): void => {
|
): void => {
|
||||||
const { eventSourceInstance } = useEventSource();
|
const { eventSourceInstance } = useEventSource();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (eventSourceInstance) {
|
if (eventSourceInstance) {
|
||||||
eventSourceInstance.addEventListener(eventName, listener);
|
eventSourceInstance.addEventListener(eventName, listener as EventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
if (eventSourceInstance) {
|
if (eventSourceInstance) {
|
||||||
eventSourceInstance.removeEventListener(eventName, listener);
|
eventSourceInstance.removeEventListener(
|
||||||
|
eventName,
|
||||||
|
listener as EventListener,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [eventName, eventSourceInstance, listener]);
|
}, [eventName, eventSourceInstance, listener]);
|
||||||
|
25
frontend/src/pages/LiveLogs/index.tsx
Normal file
25
frontend/src/pages/LiveLogs/index.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { liveLogsCompositeQuery } from 'container/LiveLogs/constants';
|
||||||
|
import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
|
import { EventSourceProvider } from 'providers/EventSource';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
function LiveLogs(): JSX.Element {
|
||||||
|
useShareBuilderUrl(liveLogsCompositeQuery);
|
||||||
|
const { handleSetConfig } = useQueryBuilder();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSetConfig(PANEL_TYPES.LIST, DataSource.LOGS);
|
||||||
|
}, [handleSetConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventSourceProvider>
|
||||||
|
<LiveLogsContainer />
|
||||||
|
</EventSourceProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LiveLogs;
|
@ -2,12 +2,15 @@ import { Col, Row } from 'antd';
|
|||||||
import ExplorerCard from 'components/ExplorerCard';
|
import ExplorerCard from 'components/ExplorerCard';
|
||||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||||
import LogsExplorerViews from 'container/LogsExplorerViews';
|
import LogsExplorerViews from 'container/LogsExplorerViews';
|
||||||
|
import LogsTopNav from 'container/LogsTopNav';
|
||||||
|
|
||||||
// ** Styles
|
// ** Styles
|
||||||
import { WrapperStyled } from './styles';
|
import { WrapperStyled } from './styles';
|
||||||
|
|
||||||
function LogsExplorer(): JSX.Element {
|
function LogsExplorer(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<LogsTopNav />
|
||||||
<WrapperStyled>
|
<WrapperStyled>
|
||||||
<Row gutter={[0, 16]}>
|
<Row gutter={[0, 16]}>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
@ -20,6 +23,7 @@ function LogsExplorer(): JSX.Element {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</WrapperStyled>
|
</WrapperStyled>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { apiV3 } from 'api/apiV1';
|
import { apiV3 } from 'api/apiV1';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
|
import { LIVE_TAIL_HEARTBEAT_TIMEOUT } from 'constants/liveTail';
|
||||||
import { EventListener, EventSourcePolyfill } from 'event-source-polyfill';
|
import { EventListener, EventSourcePolyfill } from 'event-source-polyfill';
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -18,18 +20,25 @@ interface IEventSourceContext {
|
|||||||
eventSourceInstance: EventSourcePolyfill | null;
|
eventSourceInstance: EventSourcePolyfill | null;
|
||||||
isConnectionOpen: boolean;
|
isConnectionOpen: boolean;
|
||||||
isConnectionLoading: boolean;
|
isConnectionLoading: boolean;
|
||||||
isConnectionError: string;
|
isConnectionError: boolean;
|
||||||
handleStartOpenConnection: (url?: string) => void;
|
initialLoading: boolean;
|
||||||
|
handleStartOpenConnection: (urlProps: {
|
||||||
|
url?: string;
|
||||||
|
queryString: string;
|
||||||
|
}) => void;
|
||||||
handleCloseConnection: () => void;
|
handleCloseConnection: () => void;
|
||||||
|
handleSetInitialLoading: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventSourceContext = createContext<IEventSourceContext>({
|
const EventSourceContext = createContext<IEventSourceContext>({
|
||||||
eventSourceInstance: null,
|
eventSourceInstance: null,
|
||||||
isConnectionOpen: false,
|
isConnectionOpen: false,
|
||||||
isConnectionLoading: false,
|
isConnectionLoading: false,
|
||||||
isConnectionError: '',
|
initialLoading: true,
|
||||||
|
isConnectionError: false,
|
||||||
handleStartOpenConnection: () => {},
|
handleStartOpenConnection: () => {},
|
||||||
handleCloseConnection: () => {},
|
handleCloseConnection: () => {},
|
||||||
|
handleSetInitialLoading: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function EventSourceProvider({
|
export function EventSourceProvider({
|
||||||
@ -37,72 +46,101 @@ export function EventSourceProvider({
|
|||||||
}: PropsWithChildren): JSX.Element {
|
}: PropsWithChildren): JSX.Element {
|
||||||
const [isConnectionOpen, setIsConnectionOpen] = useState<boolean>(false);
|
const [isConnectionOpen, setIsConnectionOpen] = useState<boolean>(false);
|
||||||
const [isConnectionLoading, setIsConnectionLoading] = useState<boolean>(false);
|
const [isConnectionLoading, setIsConnectionLoading] = useState<boolean>(false);
|
||||||
const [isConnectionError, setIsConnectionError] = useState<string>('');
|
const [isConnectionError, setIsConnectionError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const eventSourceRef = useRef<EventSourcePolyfill | null>(null);
|
const eventSourceRef = useRef<EventSourcePolyfill | null>(null);
|
||||||
|
|
||||||
const handleCloseConnection = useCallback(() => {
|
const handleSetInitialLoading = useCallback((value: boolean) => {
|
||||||
if (!eventSourceRef.current) return;
|
setInitialLoading(value);
|
||||||
|
|
||||||
eventSourceRef.current.close();
|
|
||||||
setIsConnectionOpen(false);
|
|
||||||
setIsConnectionLoading(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOpenConnection: EventListener = useCallback(() => {
|
const handleOpenConnection: EventListener = useCallback(() => {
|
||||||
setIsConnectionLoading(false);
|
setIsConnectionLoading(false);
|
||||||
setIsConnectionOpen(true);
|
setIsConnectionOpen(true);
|
||||||
|
setInitialLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleErrorConnection: EventListener = useCallback(() => {
|
const handleErrorConnection: EventListener = useCallback(() => {
|
||||||
|
setIsConnectionOpen(false);
|
||||||
|
setIsConnectionLoading(false);
|
||||||
|
setIsConnectionError(true);
|
||||||
|
setInitialLoading(false);
|
||||||
|
|
||||||
if (!eventSourceRef.current) return;
|
if (!eventSourceRef.current) return;
|
||||||
|
|
||||||
handleCloseConnection();
|
eventSourceRef.current.close();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const destroyEventSourceSession = useCallback(() => {
|
||||||
|
if (!eventSourceRef.current) return;
|
||||||
|
|
||||||
|
eventSourceRef.current.close();
|
||||||
eventSourceRef.current.removeEventListener('error', handleErrorConnection);
|
eventSourceRef.current.removeEventListener('error', handleErrorConnection);
|
||||||
eventSourceRef.current.removeEventListener('open', handleOpenConnection);
|
eventSourceRef.current.removeEventListener('open', handleOpenConnection);
|
||||||
}, [handleCloseConnection, handleOpenConnection]);
|
}, [handleErrorConnection, handleOpenConnection]);
|
||||||
|
|
||||||
|
const handleCloseConnection = useCallback(() => {
|
||||||
|
setIsConnectionOpen(false);
|
||||||
|
setIsConnectionLoading(false);
|
||||||
|
setIsConnectionError(false);
|
||||||
|
|
||||||
|
destroyEventSourceSession();
|
||||||
|
}, [destroyEventSourceSession]);
|
||||||
|
|
||||||
const handleStartOpenConnection = useCallback(
|
const handleStartOpenConnection = useCallback(
|
||||||
(url?: string) => {
|
(urlProps: { url?: string; queryString: string }): void => {
|
||||||
const eventSourceUrl = url || `${ENVIRONMENT.baseURL}${apiV3}logs/livetail`;
|
const { url, queryString } = urlProps;
|
||||||
|
|
||||||
const TIMEOUT_IN_MS = 10 * 60 * 1000;
|
const eventSourceUrl = url
|
||||||
|
? `${url}/?${queryString}`
|
||||||
|
: `${ENVIRONMENT.baseURL}${apiV3}logs/livetail?${queryString}`;
|
||||||
|
|
||||||
eventSourceRef.current = new EventSourcePolyfill(eventSourceUrl, {
|
eventSourceRef.current = new EventSourcePolyfill(eventSourceUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${user?.accessJwt}`,
|
Authorization: `Bearer ${user?.accessJwt}`,
|
||||||
},
|
},
|
||||||
heartbeatTimeout: TIMEOUT_IN_MS,
|
heartbeatTimeout: LIVE_TAIL_HEARTBEAT_TIMEOUT,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsConnectionLoading(true);
|
setIsConnectionLoading(true);
|
||||||
setIsConnectionError('');
|
setIsConnectionError(false);
|
||||||
|
|
||||||
eventSourceRef.current.addEventListener('error', handleErrorConnection);
|
eventSourceRef.current.addEventListener('error', handleErrorConnection);
|
||||||
|
|
||||||
eventSourceRef.current.addEventListener('open', handleOpenConnection);
|
eventSourceRef.current.addEventListener('open', handleOpenConnection);
|
||||||
},
|
},
|
||||||
[handleErrorConnection, handleOpenConnection, user?.accessJwt],
|
[user, handleErrorConnection, handleOpenConnection],
|
||||||
);
|
);
|
||||||
|
|
||||||
const contextValue = useMemo(
|
useEffect(
|
||||||
|
() => (): void => {
|
||||||
|
handleCloseConnection();
|
||||||
|
},
|
||||||
|
[handleCloseConnection],
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextValue: IEventSourceContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
eventSourceInstance: eventSourceRef.current,
|
eventSourceInstance: eventSourceRef.current,
|
||||||
isConnectionError,
|
isConnectionError,
|
||||||
isConnectionLoading,
|
isConnectionLoading,
|
||||||
isConnectionOpen,
|
isConnectionOpen,
|
||||||
|
initialLoading,
|
||||||
handleStartOpenConnection,
|
handleStartOpenConnection,
|
||||||
handleCloseConnection,
|
handleCloseConnection,
|
||||||
|
handleSetInitialLoading,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
isConnectionError,
|
isConnectionError,
|
||||||
isConnectionLoading,
|
isConnectionLoading,
|
||||||
isConnectionOpen,
|
isConnectionOpen,
|
||||||
|
initialLoading,
|
||||||
handleStartOpenConnection,
|
handleStartOpenConnection,
|
||||||
handleCloseConnection,
|
handleCloseConnection,
|
||||||
|
handleSetInitialLoading,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
|||||||
addNewQueryItem: () => {},
|
addNewQueryItem: () => {},
|
||||||
redirectWithQueryBuilderData: () => {},
|
redirectWithQueryBuilderData: () => {},
|
||||||
handleRunQuery: () => {},
|
handleRunQuery: () => {},
|
||||||
resetStagedQuery: () => {},
|
resetQuery: () => {},
|
||||||
updateAllQueriesOperators: () => initialQueriesMap.metrics,
|
updateAllQueriesOperators: () => initialQueriesMap.metrics,
|
||||||
updateQueriesData: () => initialQueriesMap.metrics,
|
updateQueriesData: () => initialQueriesMap.metrics,
|
||||||
initQueryBuilderData: () => {},
|
initQueryBuilderData: () => {},
|
||||||
@ -526,8 +526,12 @@ export function QueryBuilderProvider({
|
|||||||
});
|
});
|
||||||
}, [currentQuery, queryType, maxTime, minTime, redirectWithQueryBuilderData]);
|
}, [currentQuery, queryType, maxTime, minTime, redirectWithQueryBuilderData]);
|
||||||
|
|
||||||
const resetStagedQuery = useCallback(() => {
|
const resetQuery = useCallback((newCurrentQuery?: QueryState) => {
|
||||||
setStagedQuery(null);
|
setStagedQuery(null);
|
||||||
|
|
||||||
|
if (newCurrentQuery) {
|
||||||
|
setCurrentQuery(newCurrentQuery);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -595,7 +599,7 @@ export function QueryBuilderProvider({
|
|||||||
addNewQueryItem,
|
addNewQueryItem,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
resetStagedQuery,
|
resetQuery,
|
||||||
updateAllQueriesOperators,
|
updateAllQueriesOperators,
|
||||||
updateQueriesData,
|
updateQueriesData,
|
||||||
initQueryBuilderData,
|
initQueryBuilderData,
|
||||||
@ -618,7 +622,7 @@ export function QueryBuilderProvider({
|
|||||||
addNewQueryItem,
|
addNewQueryItem,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
resetStagedQuery,
|
resetQuery,
|
||||||
updateAllQueriesOperators,
|
updateAllQueriesOperators,
|
||||||
updateQueriesData,
|
updateQueriesData,
|
||||||
initQueryBuilderData,
|
initQueryBuilderData,
|
||||||
|
@ -3,106 +3,24 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
|
import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
|
||||||
import getStep from 'lib/getStep';
|
|
||||||
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
|
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
|
||||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import store from 'store';
|
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
|
||||||
import { Pagination } from 'hooks/queryPagination';
|
import { Pagination } from 'hooks/queryPagination';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { prepareQueryRangePayload } from './prepareQueryRangePayload';
|
||||||
|
|
||||||
export async function GetMetricQueryRange({
|
export async function GetMetricQueryRange(
|
||||||
query,
|
props: GetQueryResultsProps,
|
||||||
globalSelectedInterval,
|
): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
||||||
graphType,
|
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
|
||||||
selectedTime,
|
|
||||||
tableParams,
|
|
||||||
variables = {},
|
|
||||||
params = {},
|
|
||||||
}: GetQueryResultsProps): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
|
||||||
const queryData = query[query.queryType];
|
|
||||||
let legendMap: Record<string, string> = {};
|
|
||||||
|
|
||||||
const QueryPayload = {
|
const response = await getMetricsQueryRange(queryPayload);
|
||||||
compositeQuery: {
|
|
||||||
queryType: query.queryType,
|
|
||||||
panelType: graphType,
|
|
||||||
unit: query?.unit,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (query.queryType) {
|
|
||||||
case EQueryType.QUERY_BUILDER: {
|
|
||||||
const { queryData: data, queryFormulas } = query.builder;
|
|
||||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
|
||||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
|
||||||
|
|
||||||
const builderQueries = {
|
|
||||||
...currentQueryData.data,
|
|
||||||
...currentFormulas.data,
|
|
||||||
};
|
|
||||||
legendMap = {
|
|
||||||
...currentQueryData.newLegendMap,
|
|
||||||
...currentFormulas.newLegendMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
QueryPayload.compositeQuery.builderQueries = builderQueries;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case EQueryType.CLICKHOUSE: {
|
|
||||||
const chQueries = {};
|
|
||||||
queryData.map((query) => {
|
|
||||||
if (!query.query) return;
|
|
||||||
chQueries[query.name] = {
|
|
||||||
query: query.query,
|
|
||||||
disabled: query.disabled,
|
|
||||||
};
|
|
||||||
legendMap[query.name] = query.legend;
|
|
||||||
});
|
|
||||||
QueryPayload.compositeQuery.chQueries = chQueries;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case EQueryType.PROM: {
|
|
||||||
const promQueries = {};
|
|
||||||
queryData.map((query) => {
|
|
||||||
if (!query.query) return;
|
|
||||||
promQueries[query.name] = {
|
|
||||||
query: query.query,
|
|
||||||
disabled: query.disabled,
|
|
||||||
};
|
|
||||||
legendMap[query.name] = query.legend;
|
|
||||||
});
|
|
||||||
QueryPayload.compositeQuery.promQueries = promQueries;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { start, end } = getStartEndRangeTime({
|
|
||||||
type: selectedTime,
|
|
||||||
interval: globalSelectedInterval,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await getMetricsQueryRange({
|
|
||||||
start: parseInt(start, 10) * 1e3,
|
|
||||||
end: parseInt(end, 10) * 1e3,
|
|
||||||
step: getStep({
|
|
||||||
start: store.getState().globalTime.minTime,
|
|
||||||
end: store.getState().globalTime.maxTime,
|
|
||||||
inputFormat: 'ns',
|
|
||||||
}),
|
|
||||||
variables,
|
|
||||||
...QueryPayload,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
if (response.statusCode >= 400) {
|
if (response.statusCode >= 400) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`API responded with ${response.statusCode} - ${response.error}`,
|
`API responded with ${response.statusCode} - ${response.error}`,
|
||||||
@ -139,7 +57,7 @@ export async function GetMetricQueryRange({
|
|||||||
|
|
||||||
export interface GetQueryResultsProps {
|
export interface GetQueryResultsProps {
|
||||||
query: Query;
|
query: Query;
|
||||||
graphType: GRAPH_TYPES;
|
graphType: PANEL_TYPES;
|
||||||
selectedTime: timePreferenceType;
|
selectedTime: timePreferenceType;
|
||||||
globalSelectedInterval: Time;
|
globalSelectedInterval: Time;
|
||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
|
103
frontend/src/store/actions/dashboard/prepareQueryRangePayload.ts
Normal file
103
frontend/src/store/actions/dashboard/prepareQueryRangePayload.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
|
import store from 'store';
|
||||||
|
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
|
import { GetQueryResultsProps } from './getQueryResults';
|
||||||
|
|
||||||
|
type PrepareQueryRangePayload = {
|
||||||
|
queryPayload: QueryRangePayload;
|
||||||
|
legendMap: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareQueryRangePayload = ({
|
||||||
|
query,
|
||||||
|
globalSelectedInterval,
|
||||||
|
graphType,
|
||||||
|
selectedTime,
|
||||||
|
tableParams,
|
||||||
|
variables = {},
|
||||||
|
params = {},
|
||||||
|
}: GetQueryResultsProps): PrepareQueryRangePayload => {
|
||||||
|
let legendMap: Record<string, string> = {};
|
||||||
|
|
||||||
|
const compositeQuery: QueryRangePayload['compositeQuery'] = {
|
||||||
|
queryType: query.queryType,
|
||||||
|
panelType: graphType,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (query.queryType) {
|
||||||
|
case EQueryType.QUERY_BUILDER: {
|
||||||
|
const { queryData: data, queryFormulas } = query.builder;
|
||||||
|
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||||
|
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||||
|
|
||||||
|
const builderQueries = {
|
||||||
|
...currentQueryData.data,
|
||||||
|
...currentFormulas.data,
|
||||||
|
};
|
||||||
|
legendMap = {
|
||||||
|
...currentQueryData.newLegendMap,
|
||||||
|
...currentFormulas.newLegendMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
compositeQuery.builderQueries = builderQueries;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EQueryType.CLICKHOUSE: {
|
||||||
|
const chQueries = query[query.queryType].reduce((acc, query) => {
|
||||||
|
if (!query.query) return acc;
|
||||||
|
|
||||||
|
acc[query.name] = query;
|
||||||
|
|
||||||
|
legendMap[query.name] = query.legend;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as NonNullable<QueryRangePayload['compositeQuery']['chQueries']>);
|
||||||
|
|
||||||
|
compositeQuery.chQueries = chQueries;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EQueryType.PROM: {
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
const promQueries = query[query.queryType].reduce((acc, query) => {
|
||||||
|
if (!query.query) return acc;
|
||||||
|
|
||||||
|
acc[query.name] = query;
|
||||||
|
|
||||||
|
legendMap[query.name] = query.legend;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as NonNullable<QueryRangePayload['compositeQuery']['promQueries']>);
|
||||||
|
|
||||||
|
compositeQuery.promQueries = promQueries;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start, end } = getStartEndRangeTime({
|
||||||
|
type: selectedTime,
|
||||||
|
interval: globalSelectedInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryPayload: QueryRangePayload = {
|
||||||
|
start: parseInt(start, 10) * 1e3,
|
||||||
|
end: parseInt(end, 10) * 1e3,
|
||||||
|
step: getStep({
|
||||||
|
start: store.getState().globalTime.minTime,
|
||||||
|
end: store.getState().globalTime.maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
variables,
|
||||||
|
compositeQuery,
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { legendMap, queryPayload };
|
||||||
|
};
|
@ -1,6 +1,30 @@
|
|||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IBuilderFormula,
|
||||||
|
IBuilderQuery,
|
||||||
|
IClickHouseQuery,
|
||||||
|
IPromQLQuery,
|
||||||
|
} from '../queryBuilder/queryBuilderData';
|
||||||
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
|
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
|
||||||
|
|
||||||
export type MetricsRangeProps = never;
|
export type QueryRangePayload = {
|
||||||
|
compositeQuery: {
|
||||||
|
builderQueries?: {
|
||||||
|
[x: string]: IBuilderQuery | IBuilderFormula;
|
||||||
|
};
|
||||||
|
chQueries?: Record<string, IClickHouseQuery>;
|
||||||
|
promQueries?: Record<string, IPromQLQuery>;
|
||||||
|
queryType: EQueryType;
|
||||||
|
panelType: PANEL_TYPES;
|
||||||
|
};
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
step: number;
|
||||||
|
variables?: Record<string, unknown>;
|
||||||
|
[param: string]: unknown;
|
||||||
|
};
|
||||||
export interface MetricRangePayloadProps {
|
export interface MetricRangePayloadProps {
|
||||||
data: {
|
data: {
|
||||||
result: QueryData[];
|
result: QueryData[];
|
||||||
|
@ -9,7 +9,7 @@ export interface BaseAutocompleteData {
|
|||||||
dataType: DataType;
|
dataType: DataType;
|
||||||
isColumn: boolean;
|
isColumn: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
type: AutocompleteType;
|
type: AutocompleteType | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQueryAutocompleteResponse {
|
export interface IQueryAutocompleteResponse {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
IClickHouseQuery,
|
IClickHouseQuery,
|
||||||
IPromQLQuery,
|
IPromQLQuery,
|
||||||
Query,
|
Query,
|
||||||
|
QueryState,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { EQueryType } from './dashboard';
|
import { EQueryType } from './dashboard';
|
||||||
@ -187,8 +188,8 @@ export type QueryBuilderContextType = {
|
|||||||
searchParams?: Record<string, unknown>,
|
searchParams?: Record<string, unknown>,
|
||||||
) => void;
|
) => void;
|
||||||
handleRunQuery: () => void;
|
handleRunQuery: () => void;
|
||||||
resetStagedQuery: () => void;
|
|
||||||
handleOnUnitsChange: (units: Format['id']) => void;
|
handleOnUnitsChange: (units: Format['id']) => void;
|
||||||
|
resetQuery: (newCurrentQuery?: QueryState) => void;
|
||||||
updateAllQueriesOperators: (
|
updateAllQueriesOperators: (
|
||||||
queryData: Query,
|
queryData: Query,
|
||||||
panelType: PANEL_TYPES,
|
panelType: PANEL_TYPES,
|
||||||
|
@ -73,6 +73,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
VERSION: ['ADMIN', 'EDITOR', 'VIEWER'],
|
VERSION: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
LIVE_LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
LIST_LICENSES: ['ADMIN'],
|
LIST_LICENSES: ['ADMIN'],
|
||||||
LOGS_INDEX_FIELDS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
LOGS_INDEX_FIELDS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
LOGS_PIPELINE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
LOGS_PIPELINE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user