mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 02:15:57 +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",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"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",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
|
@ -110,6 +110,10 @@ export const LogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
|
||||
);
|
||||
|
||||
export const LiveLogs = Loadable(
|
||||
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
|
||||
);
|
||||
|
||||
export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
GettingStarted,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
Login,
|
||||
Logs,
|
||||
LogsExplorer,
|
||||
@ -234,6 +235,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIVE_LOGS,
|
||||
exact: true,
|
||||
component: LiveLogs,
|
||||
key: 'LIVE_LOGS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGIN,
|
||||
exact: true,
|
||||
|
@ -4,11 +4,11 @@ import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadV3,
|
||||
MetricsRangeProps,
|
||||
QueryRangePayload,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export const getMetricsQueryRange = async (
|
||||
props: MetricsRangeProps,
|
||||
props: QueryRangePayload,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||
try {
|
||||
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',
|
||||
LOGS: '/logs',
|
||||
LOGS_EXPLORER: '/logs-explorer',
|
||||
LIVE_LOGS: '/logs-explorer/live',
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
LIST_LICENSES: '/licenses',
|
||||
|
@ -52,6 +52,8 @@ const themeColors = {
|
||||
gamboge: '#D89614',
|
||||
bckgGrey: '#1d1d1d',
|
||||
lightBlue: '#177ddc',
|
||||
buttonSuccessRgb: '73, 170, 25',
|
||||
red: '#E84749',
|
||||
};
|
||||
|
||||
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 = {
|
||||
data: QueryData[];
|
||||
isLoading: boolean;
|
||||
isLabelEnabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getChartData, { GetChartDataProps } from 'lib/getChartData';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||
import { CardStyled } from './LogsExplorerChart.styled';
|
||||
@ -10,17 +11,22 @@ import { CardStyled } from './LogsExplorerChart.styled';
|
||||
function LogsExplorerChart({
|
||||
data,
|
||||
isLoading,
|
||||
isLabelEnabled = true,
|
||||
className,
|
||||
}: LogsExplorerChartProps): JSX.Element {
|
||||
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = (
|
||||
element,
|
||||
index,
|
||||
allLabels,
|
||||
) => ({
|
||||
label: allLabels[index],
|
||||
data: element,
|
||||
backgroundColor: colors[index % colors.length] || 'red',
|
||||
borderColor: colors[index % colors.length] || 'red',
|
||||
});
|
||||
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = useCallback(
|
||||
(element, index, allLabels) => ({
|
||||
data: element,
|
||||
backgroundColor: colors[index % colors.length] || themeColors.red,
|
||||
borderColor: colors[index % colors.length] || themeColors.red,
|
||||
...(isLabelEnabled
|
||||
? {
|
||||
label: allLabels[index],
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
[isLabelEnabled],
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
() =>
|
||||
@ -32,11 +38,11 @@ function LogsExplorerChart({
|
||||
],
|
||||
createDataset: handleCreateDatasets,
|
||||
}),
|
||||
[data],
|
||||
[data, handleCreateDatasets],
|
||||
);
|
||||
|
||||
return (
|
||||
<CardStyled>
|
||||
<CardStyled className={className}>
|
||||
{isLoading ? (
|
||||
<Spinner size="default" height="100%" />
|
||||
) : (
|
||||
|
@ -10,6 +10,7 @@ import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import {
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
memo,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
@ -67,7 +68,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const { onEndReached } = infitiyTableProps;
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: onSetActiveLog,
|
||||
@ -158,8 +158,11 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
fixedHeaderContent={tableHeader}
|
||||
endReached={onEndReached}
|
||||
totalCount={dataSource.length}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(infitiyTableProps?.onEndReached
|
||||
? { endReached: infitiyTableProps.onEndReached }
|
||||
: {})}
|
||||
/>
|
||||
|
||||
{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 = {
|
||||
isLoading?: boolean;
|
||||
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
|
||||
infitiyTableProps: {
|
||||
infitiyTableProps?: {
|
||||
onEndReached: (index: number) => void;
|
||||
};
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ import {
|
||||
Query,
|
||||
TagFilter,
|
||||
} 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 { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
|
||||
@ -120,7 +120,7 @@ function LogsExplorerViews(): JSX.Element {
|
||||
|
||||
const modifiedQueryData: IBuilderQuery = {
|
||||
...listQuery,
|
||||
aggregateOperator: StringOperators.COUNT,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
};
|
||||
|
||||
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 { FieldTitle } from '../styles';
|
||||
@ -7,6 +9,15 @@ import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
if (!config) return;
|
||||
|
||||
config.onChange(event.target.value);
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
@ -16,7 +27,7 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
size="small"
|
||||
buttonStyle="solid"
|
||||
value={config.value}
|
||||
onChange={config.onChange}
|
||||
onChange={onChange}
|
||||
>
|
||||
<RadioButton value="raw">{t('options_menu.raw')}</RadioButton>
|
||||
<RadioButton value="list">{t('options_menu.default')}</RadioButton>
|
||||
|
@ -14,7 +14,9 @@ export interface InitialOptions
|
||||
}
|
||||
|
||||
export type OptionsMenuConfig = {
|
||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||
format?: Pick<RadioProps, 'value'> & {
|
||||
onChange: (value: LogViewMode) => void;
|
||||
};
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
addColumn?: Pick<
|
||||
SelectProps,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { RadioChangeEvent } from 'antd';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@ -213,10 +213,10 @@ const useOptionsMenu = ({
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
(value: LogViewMode) => {
|
||||
const optionsData: OptionsQuery = {
|
||||
...optionsQueryData,
|
||||
format: event.target.value,
|
||||
format: value,
|
||||
};
|
||||
|
||||
handleRedirectWithOptionsData(optionsData);
|
||||
|
@ -35,6 +35,7 @@ function QueryBuilderSearch({
|
||||
query,
|
||||
onChange,
|
||||
whereClauseConfig,
|
||||
className,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
const {
|
||||
updateTag,
|
||||
@ -163,6 +164,7 @@ function QueryBuilderSearch({
|
||||
placeholder={PLACEHOLDER}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={className}
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
@ -186,10 +188,12 @@ interface QueryBuilderSearchProps {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: TagFilter) => void;
|
||||
whereClauseConfig?: WhereClauseConfig;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
QueryBuilderSearch.defaultProps = {
|
||||
whereClauseConfig: undefined,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export interface CustomTagProps {
|
||||
|
@ -21,6 +21,7 @@ const breadcrumbNameMap = {
|
||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||
[ROUTES.LOGS]: 'Logs',
|
||||
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
|
||||
[ROUTES.LIVE_LOGS]: 'Live View',
|
||||
[ROUTES.PIPELINES]: 'Pipelines',
|
||||
};
|
||||
|
||||
|
@ -84,3 +84,5 @@ export const routesToSkip = [
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
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 { matchPath, useHistory } from 'react-router-dom';
|
||||
|
||||
import NewExplorerCTA from '../NewExplorerCTA';
|
||||
import ShowBreadcrumbs from './Breadcrumbs';
|
||||
import DateTimeSelector from './DateTimeSelection';
|
||||
import { routesToSkip } from './DateTimeSelection/config';
|
||||
import NewExplorerCTA from './NewExplorerCTA';
|
||||
import { routesToDisable, routesToSkip } from './DateTimeSelection/config';
|
||||
import { Container } from './styles';
|
||||
|
||||
function TopNav(): JSX.Element | null {
|
||||
@ -20,12 +20,20 @@ function TopNav(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() =>
|
||||
routesToDisable.some((route) =>
|
||||
matchPath(location.pathname, { path: route, exact: true }),
|
||||
),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const isSignUpPage = useMemo(
|
||||
() => matchPath(location.pathname, { path: ROUTES.SIGN_UP, exact: true }),
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
if (isSignUpPage) {
|
||||
if (isSignUpPage || isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -40,7 +48,6 @@ function TopNav(): JSX.Element | null {
|
||||
<Row justify="end">
|
||||
<Space align="start" size={60} direction="horizontal">
|
||||
<NewExplorerCTA />
|
||||
|
||||
<div>
|
||||
<DateTimeSelector />
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ import { useQueryBuilder } from './useQueryBuilder';
|
||||
export type UseShareBuilderUrlParams = { defaultValue: Query };
|
||||
|
||||
export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
|
||||
const { redirectWithQueryBuilderData, resetQuery } = useQueryBuilder();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
@ -21,8 +21,8 @@ export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
resetStagedQuery();
|
||||
resetQuery();
|
||||
},
|
||||
[resetStagedQuery],
|
||||
[resetQuery],
|
||||
);
|
||||
};
|
||||
|
@ -2,20 +2,29 @@ import { EventListener, EventSourceEventMap } from 'event-source-polyfill';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useEventSourceEvent = (
|
||||
eventName: keyof EventSourceEventMap,
|
||||
listener: EventListener,
|
||||
type EventMap = {
|
||||
message: MessageEvent;
|
||||
open: Event;
|
||||
error: Event;
|
||||
};
|
||||
|
||||
export const useEventSourceEvent = <T extends keyof EventSourceEventMap>(
|
||||
eventName: T,
|
||||
listener: (event: EventMap[T]) => void,
|
||||
): void => {
|
||||
const { eventSourceInstance } = useEventSource();
|
||||
|
||||
useEffect(() => {
|
||||
if (eventSourceInstance) {
|
||||
eventSourceInstance.addEventListener(eventName, listener);
|
||||
eventSourceInstance.addEventListener(eventName, listener as EventListener);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
if (eventSourceInstance) {
|
||||
eventSourceInstance.removeEventListener(eventName, listener);
|
||||
eventSourceInstance.removeEventListener(
|
||||
eventName,
|
||||
listener as EventListener,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [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,24 +2,28 @@ import { Col, Row } from 'antd';
|
||||
import ExplorerCard from 'components/ExplorerCard';
|
||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||
import LogsExplorerViews from 'container/LogsExplorerViews';
|
||||
import LogsTopNav from 'container/LogsTopNav';
|
||||
|
||||
// ** Styles
|
||||
import { WrapperStyled } from './styles';
|
||||
|
||||
function LogsExplorer(): JSX.Element {
|
||||
return (
|
||||
<WrapperStyled>
|
||||
<Row gutter={[0, 16]}>
|
||||
<Col xs={24}>
|
||||
<ExplorerCard>
|
||||
<LogExplorerQuerySection />
|
||||
</ExplorerCard>
|
||||
</Col>
|
||||
<Col xs={24}>
|
||||
<LogsExplorerViews />
|
||||
</Col>
|
||||
</Row>
|
||||
</WrapperStyled>
|
||||
<>
|
||||
<LogsTopNav />
|
||||
<WrapperStyled>
|
||||
<Row gutter={[0, 16]}>
|
||||
<Col xs={24}>
|
||||
<ExplorerCard>
|
||||
<LogExplorerQuerySection />
|
||||
</ExplorerCard>
|
||||
</Col>
|
||||
<Col xs={24}>
|
||||
<LogsExplorerViews />
|
||||
</Col>
|
||||
</Row>
|
||||
</WrapperStyled>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { apiV3 } from 'api/apiV1';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LIVE_TAIL_HEARTBEAT_TIMEOUT } from 'constants/liveTail';
|
||||
import { EventListener, EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@ -18,18 +20,25 @@ interface IEventSourceContext {
|
||||
eventSourceInstance: EventSourcePolyfill | null;
|
||||
isConnectionOpen: boolean;
|
||||
isConnectionLoading: boolean;
|
||||
isConnectionError: string;
|
||||
handleStartOpenConnection: (url?: string) => void;
|
||||
isConnectionError: boolean;
|
||||
initialLoading: boolean;
|
||||
handleStartOpenConnection: (urlProps: {
|
||||
url?: string;
|
||||
queryString: string;
|
||||
}) => void;
|
||||
handleCloseConnection: () => void;
|
||||
handleSetInitialLoading: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const EventSourceContext = createContext<IEventSourceContext>({
|
||||
eventSourceInstance: null,
|
||||
isConnectionOpen: false,
|
||||
isConnectionLoading: false,
|
||||
isConnectionError: '',
|
||||
initialLoading: true,
|
||||
isConnectionError: false,
|
||||
handleStartOpenConnection: () => {},
|
||||
handleCloseConnection: () => {},
|
||||
handleSetInitialLoading: () => {},
|
||||
});
|
||||
|
||||
export function EventSourceProvider({
|
||||
@ -37,72 +46,101 @@ export function EventSourceProvider({
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
const [isConnectionOpen, setIsConnectionOpen] = 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 eventSourceRef = useRef<EventSourcePolyfill | null>(null);
|
||||
|
||||
const handleCloseConnection = useCallback(() => {
|
||||
if (!eventSourceRef.current) return;
|
||||
|
||||
eventSourceRef.current.close();
|
||||
setIsConnectionOpen(false);
|
||||
setIsConnectionLoading(false);
|
||||
const handleSetInitialLoading = useCallback((value: boolean) => {
|
||||
setInitialLoading(value);
|
||||
}, []);
|
||||
|
||||
const handleOpenConnection: EventListener = useCallback(() => {
|
||||
setIsConnectionLoading(false);
|
||||
setIsConnectionOpen(true);
|
||||
setInitialLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleErrorConnection: EventListener = useCallback(() => {
|
||||
setIsConnectionOpen(false);
|
||||
setIsConnectionLoading(false);
|
||||
setIsConnectionError(true);
|
||||
setInitialLoading(false);
|
||||
|
||||
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('open', handleOpenConnection);
|
||||
}, [handleCloseConnection, handleOpenConnection]);
|
||||
}, [handleErrorConnection, handleOpenConnection]);
|
||||
|
||||
const handleCloseConnection = useCallback(() => {
|
||||
setIsConnectionOpen(false);
|
||||
setIsConnectionLoading(false);
|
||||
setIsConnectionError(false);
|
||||
|
||||
destroyEventSourceSession();
|
||||
}, [destroyEventSourceSession]);
|
||||
|
||||
const handleStartOpenConnection = useCallback(
|
||||
(url?: string) => {
|
||||
const eventSourceUrl = url || `${ENVIRONMENT.baseURL}${apiV3}logs/livetail`;
|
||||
(urlProps: { url?: string; queryString: string }): void => {
|
||||
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, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${user?.accessJwt}`,
|
||||
},
|
||||
heartbeatTimeout: TIMEOUT_IN_MS,
|
||||
heartbeatTimeout: LIVE_TAIL_HEARTBEAT_TIMEOUT,
|
||||
});
|
||||
|
||||
setIsConnectionLoading(true);
|
||||
setIsConnectionError('');
|
||||
setIsConnectionError(false);
|
||||
|
||||
eventSourceRef.current.addEventListener('error', handleErrorConnection);
|
||||
|
||||
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,
|
||||
isConnectionError,
|
||||
isConnectionLoading,
|
||||
isConnectionOpen,
|
||||
initialLoading,
|
||||
handleStartOpenConnection,
|
||||
handleCloseConnection,
|
||||
handleSetInitialLoading,
|
||||
}),
|
||||
[
|
||||
isConnectionError,
|
||||
isConnectionLoading,
|
||||
isConnectionOpen,
|
||||
initialLoading,
|
||||
handleStartOpenConnection,
|
||||
handleCloseConnection,
|
||||
handleSetInitialLoading,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -67,7 +67,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||
addNewQueryItem: () => {},
|
||||
redirectWithQueryBuilderData: () => {},
|
||||
handleRunQuery: () => {},
|
||||
resetStagedQuery: () => {},
|
||||
resetQuery: () => {},
|
||||
updateAllQueriesOperators: () => initialQueriesMap.metrics,
|
||||
updateQueriesData: () => initialQueriesMap.metrics,
|
||||
initQueryBuilderData: () => {},
|
||||
@ -526,8 +526,12 @@ export function QueryBuilderProvider({
|
||||
});
|
||||
}, [currentQuery, queryType, maxTime, minTime, redirectWithQueryBuilderData]);
|
||||
|
||||
const resetStagedQuery = useCallback(() => {
|
||||
const resetQuery = useCallback((newCurrentQuery?: QueryState) => {
|
||||
setStagedQuery(null);
|
||||
|
||||
if (newCurrentQuery) {
|
||||
setCurrentQuery(newCurrentQuery);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -595,7 +599,7 @@ export function QueryBuilderProvider({
|
||||
addNewQueryItem,
|
||||
redirectWithQueryBuilderData,
|
||||
handleRunQuery,
|
||||
resetStagedQuery,
|
||||
resetQuery,
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
initQueryBuilderData,
|
||||
@ -618,7 +622,7 @@ export function QueryBuilderProvider({
|
||||
addNewQueryItem,
|
||||
redirectWithQueryBuilderData,
|
||||
handleRunQuery,
|
||||
resetStagedQuery,
|
||||
resetQuery,
|
||||
updateAllQueriesOperators,
|
||||
updateQueriesData,
|
||||
initQueryBuilderData,
|
||||
|
@ -3,106 +3,24 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import getStep from 'lib/getStep';
|
||||
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import store from 'store';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { prepareQueryRangePayload } from './prepareQueryRangePayload';
|
||||
|
||||
export async function GetMetricQueryRange({
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
params = {},
|
||||
}: GetQueryResultsProps): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
||||
const queryData = query[query.queryType];
|
||||
let legendMap: Record<string, string> = {};
|
||||
export async function GetMetricQueryRange(
|
||||
props: GetQueryResultsProps,
|
||||
): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
||||
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
|
||||
|
||||
const QueryPayload = {
|
||||
compositeQuery: {
|
||||
queryType: query.queryType,
|
||||
panelType: graphType,
|
||||
unit: query?.unit,
|
||||
},
|
||||
};
|
||||
const response = await getMetricsQueryRange(queryPayload);
|
||||
|
||||
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) {
|
||||
throw new Error(
|
||||
`API responded with ${response.statusCode} - ${response.error}`,
|
||||
@ -139,7 +57,7 @@ export async function GetMetricQueryRange({
|
||||
|
||||
export interface GetQueryResultsProps {
|
||||
query: Query;
|
||||
graphType: GRAPH_TYPES;
|
||||
graphType: PANEL_TYPES;
|
||||
selectedTime: timePreferenceType;
|
||||
globalSelectedInterval: Time;
|
||||
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';
|
||||
|
||||
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 {
|
||||
data: {
|
||||
result: QueryData[];
|
||||
|
@ -9,7 +9,7 @@ export interface BaseAutocompleteData {
|
||||
dataType: DataType;
|
||||
isColumn: boolean;
|
||||
key: string;
|
||||
type: AutocompleteType;
|
||||
type: AutocompleteType | string | null;
|
||||
}
|
||||
|
||||
export interface IQueryAutocompleteResponse {
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
Query,
|
||||
QueryState,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { EQueryType } from './dashboard';
|
||||
@ -187,8 +188,8 @@ export type QueryBuilderContextType = {
|
||||
searchParams?: Record<string, unknown>,
|
||||
) => void;
|
||||
handleRunQuery: () => void;
|
||||
resetStagedQuery: () => void;
|
||||
handleOnUnitsChange: (units: Format['id']) => void;
|
||||
resetQuery: (newCurrentQuery?: QueryState) => void;
|
||||
updateAllQueriesOperators: (
|
||||
queryData: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
|
@ -73,6 +73,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
VERSION: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LIVE_LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LIST_LICENSES: ['ADMIN'],
|
||||
LOGS_INDEX_FIELDS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS_PIPELINE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
Loading…
x
Reference in New Issue
Block a user