Feat/list infinity scroll (#2992)

* feat: add custom orderBy

* feat: infinity scroll list logs list

* feat: add infinity table view

* Fix/double query logs request (#3006)

* feat: add control panel

* fix: repeating query api request

* fix: scroll, remove id, page size

* fix: reset offset to 0

* feat: add log explorer detail (#3007)

* feat: add control panel

* fix: repeating query api request

* feat: add log explorer detail

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>

* feat: add group by in the logs chart (#3009)

* feat: add control panel

* fix: repeating query api request

* feat: add log explorer detail

* feat: add group by in the logs chart

* fix: list timestamp, limit, filter order

* feat: add list chart (#3037)

* feat: add list chart

* refactor: remove console log

* feat: hide aggregate every for table view (#3046)

* feat: hide aggregate every for table view

* fix: text filter for inactive filters

* refactor: remove log

* fix: table columns

* fix: timestamp type

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
This commit is contained in:
Yevhen Shevchenko 2023-07-06 14:22:44 +03:00 committed by GitHub
parent 8363dadd8d
commit 76ba364317
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1291 additions and 501 deletions

View File

@ -0,0 +1,3 @@
import { ILog } from 'types/api/logs/log';
export type LogDetailProps = { log: ILog | null; onClose: () => void };

View File

@ -0,0 +1,41 @@
import { Drawer, Tabs } from 'antd';
import JSONView from 'container/LogDetailedView/JsonView';
import TableView from 'container/LogDetailedView/TableView';
import { LogDetailProps } from './LogDetail.interfaces';
function LogDetail({ log, onClose }: LogDetailProps): JSX.Element {
const onDrawerClose = (): void => {
onClose();
};
const items = [
{
label: 'Table',
key: '1',
children: log && <TableView logData={log} />,
},
{
label: 'JSON',
key: '2',
children: log && <JSONView logData={log} />,
},
];
return (
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
onClose={onDrawerClose}
open={log !== null}
style={{ overscrollBehavior: 'contain' }}
destroyOnClose
>
<Tabs defaultActiveKey="1" items={items} />
</Drawer>
);
}
export default LogDetail;

View File

@ -8,13 +8,10 @@ import { useNotifications } from 'hooks/useNotifications';
// utils // utils
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
// interfaces // interfaces
import { AppState } from 'store/reducers'; import { IField } from 'types/api/logs/fields';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs';
// components // components
import AddToQueryHOC from '../AddToQueryHOC'; import AddToQueryHOC from '../AddToQueryHOC';
@ -79,24 +76,22 @@ function LogSelectedField({
interface ListLogViewProps { interface ListLogViewProps {
logData: ILog; logData: ILog;
onOpenDetailedView: (log: ILog) => void;
selectedFields: IField[];
} }
function ListLogView({ logData }: ListLogViewProps): JSX.Element { function ListLogView({
const { logData,
fields: { selected }, selectedFields,
} = useSelector<AppState, ILogsReducer>((state) => state.logs); onOpenDetailedView,
}: ListLogViewProps): JSX.Element {
const dispatch = useDispatch();
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard(); const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const handleDetailedView = useCallback(() => { const handleDetailedView = useCallback(() => {
dispatch({ onOpenDetailedView(logData);
type: SET_DETAILED_LOG_DATA, }, [logData, onOpenDetailedView]);
payload: logData,
});
}, [dispatch, logData]);
const handleCopyJSON = (): void => { const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2)); setCopy(JSON.stringify(logData, null, 2));
@ -106,8 +101,16 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
}; };
const updatedSelecedFields = useMemo( const updatedSelecedFields = useMemo(
() => selected.filter((e) => e.name !== 'id'), () => selectedFields.filter((e) => e.name !== 'id'),
[selected], [selectedFields],
);
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format()
: dayjs(flattenLogData.timestamp / 1e6).format(),
[flattenLogData.timestamp],
); );
return ( return (
@ -119,10 +122,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
{flattenLogData.stream && ( {flattenLogData.stream && (
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} /> <LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)} )}
<LogGeneralField <LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</> </>
</LogContainer> </LogContainer>
<div> <div>

View File

@ -30,7 +30,10 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const text = useMemo( const text = useMemo(
() => `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`, () =>
typeof data.timestamp === 'string'
? `${dayjs(data.timestamp).format()} | ${data.body}`
: `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`,
[data.timestamp, data.body], [data.timestamp, data.body],
); );

View File

@ -1,121 +1,18 @@
import { ExpandAltOutlined } from '@ant-design/icons'; import { Table } from 'antd';
import Convert from 'ansi-to-html';
import { Table, Typography } from 'antd';
import { ColumnsType, ColumnType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useMemo } from 'react';
import { IField } from 'types/api/logs/fields';
// interfaces
import { ILog } from 'types/api/logs/log';
// styles
import { ExpandIconWrapper } from '../RawLogView/styles';
// config // config
import { defaultCellStyle, defaultTableStyle, tableScroll } from './config'; import { tableScroll } from './config';
import { TableBodyContent } from './styles'; import { LogsTableViewProps } from './types';
import { useTableView } from './useTableView';
type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
onClickExpand: (log: ILog) => void;
};
const convert = new Convert();
function LogsTableView(props: LogsTableViewProps): JSX.Element { function LogsTableView(props: LogsTableViewProps): JSX.Element {
const { logs, fields, linesPerRow, onClickExpand } = props; const { dataSource, columns } = useTableView(props);
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
.map(({ name }) => ({
title: name,
dataIndex: name,
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
{field}
</Typography.Paragraph>
),
}),
}));
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
onClickExpand((item as unknown) as ILog);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
const date = dayjs(field / 1e6).format();
return {
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
};
},
},
...fieldColumns,
{
title: 'body',
dataIndex: 'body',
key: 'body',
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultTableStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: convert.toHtml(dompurify.sanitize(field)),
}}
linesPerRow={linesPerRow}
/>
),
}),
},
];
}, [fields, linesPerRow, onClickExpand]);
return ( return (
<Table <Table
size="small" size="small"
columns={columns} columns={columns}
dataSource={flattenLogData} dataSource={dataSource}
pagination={false} pagination={false}
rowKey="id" rowKey="id"
bordered bordered

View File

@ -0,0 +1,14 @@
import { ColumnType } from 'antd/es/table';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
export type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
onClickExpand: (log: ILog) => void;
};

View File

@ -0,0 +1,108 @@
import { ExpandAltOutlined } from '@ant-design/icons';
import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useMemo } from 'react';
import { ILog } from 'types/api/logs/log';
import { ExpandIconWrapper } from '../RawLogView/styles';
import { defaultCellStyle, defaultTableStyle } from './config';
import { TableBodyContent } from './styles';
import { ColumnTypeRender, LogsTableViewProps } from './types';
export type UseTableViewResult = {
columns: ColumnsType<Record<string, unknown>>;
dataSource: Record<string, string>[];
};
const convert = new Convert();
export const useTableView = (props: LogsTableViewProps): UseTableViewResult => {
const { logs, fields, linesPerRow, onClickExpand } = props;
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
.map(({ name }) => ({
title: name,
dataIndex: name,
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
{field}
</Typography.Paragraph>
),
}),
}));
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
onClickExpand((item as unknown) as ILog);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format()
: dayjs(field / 1e6).format();
return {
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
};
},
},
...fieldColumns,
{
title: 'body',
dataIndex: 'body',
key: 'body',
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultTableStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: convert.toHtml(dompurify.sanitize(field)),
}}
linesPerRow={linesPerRow}
/>
),
}),
},
];
}, [fields, linesPerRow, onClickExpand]);
return { columns, dataSource: flattenLogData };
};

View File

@ -14,6 +14,7 @@ import {
IPromQLQuery, IPromQLQuery,
Query, Query,
QueryState, QueryState,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData'; } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { import {
@ -113,6 +114,11 @@ export const initialAutocompleteData: BaseAutocompleteData = {
type: null, type: null,
}; };
export const initialFilters: TagFilter = {
items: [],
op: 'AND',
};
const initialQueryBuilderFormValues: IBuilderQuery = { const initialQueryBuilderFormValues: IBuilderQuery = {
dataSource: DataSource.METRICS, dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),

View File

@ -1,2 +1,16 @@
export const COMPOSITE_QUERY = 'compositeQuery'; type QueryParamNames =
export const PANEL_TYPES_QUERY = 'panelTypes'; | 'compositeQuery'
| 'panelTypes'
| 'pageSize'
| 'viewMode'
| 'selectedFields'
| 'linesPerRow';
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
compositeQuery: 'compositeQuery',
panelTypes: 'panelTypes',
pageSize: 'pageSize',
viewMode: 'viewMode',
selectedFields: 'selectedFields',
linesPerRow: 'linesPerRow',
};

View File

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

View File

@ -0,0 +1,7 @@
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
export type ExplorerControlPanelProps = {
isShowPageSize: boolean;
isLoading: boolean;
optionsMenuConfig?: OptionsMenuConfig;
};

View File

@ -0,0 +1,29 @@
import { Col, Row } from 'antd';
import OptionsMenu from 'container/OptionsMenu';
import PageSizeSelect from 'container/PageSizeSelect';
import { ExplorerControlPanelProps } from './ExplorerControlPanel.interfaces';
import { ContainerStyled } from './styles';
function ExplorerControlPanel({
isLoading,
isShowPageSize,
optionsMenuConfig,
}: ExplorerControlPanelProps): JSX.Element {
return (
<ContainerStyled>
<Row justify="end" gutter={30}>
{optionsMenuConfig && (
<Col>
<OptionsMenu config={optionsMenuConfig} />
</Col>
)}
<Col>
<PageSizeSelect isLoading={isLoading} isShow={isShowPageSize} />
</Col>
</Row>
</ContainerStyled>
);
}
export default ExplorerControlPanel;

View File

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

View File

@ -1,5 +1,5 @@
import { Button, Dropdown, MenuProps, Modal } from 'antd'; import { Button, Dropdown, MenuProps, Modal } from 'antd';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import history from 'lib/history'; import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@ -22,9 +22,9 @@ function ExportPanel({
const onCreateAlertsHandler = useCallback(() => { const onCreateAlertsHandler = useCallback(() => {
history.push( history.push(
`${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent( `${ROUTES.ALERTS_NEW}?${
JSON.stringify(query), queryParamNamesMap.compositeQuery
)}`, }=${encodeURIComponent(JSON.stringify(query))}`,
); );
}, [query]); }, [query]);

View File

@ -9,7 +9,7 @@ import {
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import { MenuItemType } from 'antd/es/menu/hooks/useItems'; import { MenuItemType } from 'antd/es/menu/hooks/useItems';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history'; import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@ -64,7 +64,9 @@ function WidgetHeader({
history.push( history.push(
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${ `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
widget.panelTypes widget.panelTypes
}&${COMPOSITE_QUERY}=${encodeURIComponent(JSON.stringify(widget.query))}`, }&${queryParamNamesMap.compositeQuery}=${encodeURIComponent(
JSON.stringify(widget.query),
)}`,
); );
}, [widget.id, widget.panelTypes, widget.query]); }, [widget.id, widget.panelTypes, widget.query]);

View File

@ -5,7 +5,7 @@ import { ColumnsType } from 'antd/lib/table';
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import useInterval from 'hooks/useInterval'; import useInterval from 'hooks/useInterval';
@ -75,11 +75,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery); const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
history.push( history.push(
`${ `${ROUTES.EDIT_ALERTS}?ruleId=${record.id.toString()}&${
ROUTES.EDIT_ALERTS queryParamNamesMap.compositeQuery
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent( }=${encodeURIComponent(JSON.stringify(compositeQuery))}`,
JSON.stringify(compositeQuery),
)}`,
); );
}) })
.catch(handleError); .catch(handleError);

View File

@ -83,12 +83,17 @@ function LogControls(): JSX.Element | null {
const flattenLogData = useMemo( const flattenLogData = useMemo(
() => () =>
logs.map((log) => logs.map((log) => {
FlatLogData({ const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
return FlatLogData({
...log, ...log,
timestamp: (dayjs(log.timestamp / 1e6).format() as unknown) as number, timestamp,
}), });
), }),
[logs], [logs],
); );

View File

@ -1,4 +1,4 @@
import { Drawer, Tabs } from 'antd'; import LogDetail from 'components/LogDetail';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -6,9 +6,6 @@ import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import JSONView from './JsonView';
import TableView from './TableView';
function LogDetailedView(): JSX.Element { function LogDetailedView(): JSX.Element {
const { detailedLog } = useSelector<AppState, ILogsReducer>( const { detailedLog } = useSelector<AppState, ILogsReducer>(
(state) => state.logs, (state) => state.logs,
@ -23,33 +20,7 @@ function LogDetailedView(): JSX.Element {
}); });
}; };
const items = [ return <LogDetail log={detailedLog} onClose={onDrawerClose} />;
{
label: 'Table',
key: '1',
children: detailedLog && <TableView logData={detailedLog} />,
},
{
label: 'JSON',
key: '2',
children: detailedLog && <JSONView logData={detailedLog} />,
},
];
return (
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
onClose={onDrawerClose}
open={detailedLog !== null}
style={{ overscrollBehavior: 'contain' }}
destroyOnClose
>
<Tabs defaultActiveKey="1" items={items} />
</Drawer>
);
} }
export default LogDetailedView; export default LogDetailedView;

View File

@ -0,0 +1,6 @@
import { ILog } from 'types/api/logs/log';
export type LogExplorerDetailedViewProps = {
log: ILog | null;
onClose: () => void;
};

View File

@ -0,0 +1,16 @@
import LogDetail from 'components/LogDetail';
import { LogExplorerDetailedViewProps } from './LogExplorerDetailedView.interfaces';
function LogExplorerDetailedView({
log,
onClose,
}: LogExplorerDetailedViewProps): JSX.Element {
const onDrawerClose = (): void => {
onClose();
};
return <LogDetail log={log} onClose={onDrawerClose} />;
}
export default LogExplorerDetailedView;

View File

@ -0,0 +1,6 @@
import { QueryData } from 'types/api/widgets/getQuery';
export type LogsExplorerChartProps = {
data: QueryData[];
isLoading: boolean;
};

View File

@ -1,48 +1,43 @@
import Graph from 'components/Graph'; import Graph from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import getChartData, { GetChartDataProps } from 'lib/getChartData';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { colors } from 'lib/getRandomColor';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
import { CardStyled } from './LogsExplorerChart.styled'; import { CardStyled } from './LogsExplorerChart.styled';
function LogsExplorerChart(): JSX.Element { function LogsExplorerChart({
const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder(); data,
isLoading,
}: 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 { selectedTime } = useSelector<AppState, GlobalReducer>( const graphData = useMemo(
(state) => state.globalTime, () =>
getChartData({
queryData: [
{
queryData: data,
},
],
createDataset: handleCreateDatasets,
}),
[data],
); );
const { data, isFetching } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelType || PANEL_TYPES.LIST,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
},
{
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
enabled: isEnabledQuery,
},
);
const graphData = useMemo(() => {
if (data?.payload.data && data.payload.data.result.length > 0) {
return getExplorerChartData([data.payload.data.result[0]]);
}
return getExplorerChartData([]);
}, [data]);
return ( return (
<CardStyled> <CardStyled>
{isFetching ? ( {isLoading ? (
<Spinner size="default" height="100%" /> <Spinner size="default" height="100%" />
) : ( ) : (
<Graph <Graph

View File

@ -0,0 +1,6 @@
import { CSSProperties } from 'react';
export const infinityDefaultStyles: CSSProperties = {
height: 'auto',
width: '100%',
};

View File

@ -0,0 +1,98 @@
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { cloneElement, ReactElement, ReactNode, useCallback } from 'react';
import { TableComponents, TableVirtuoso } from 'react-virtuoso';
import { infinityDefaultStyles } from './config';
import {
TableCellStyled,
TableHeaderCellStyled,
TableRowStyled,
TableStyled,
} from './styles';
import { InfinityTableProps } from './types';
// eslint-disable-next-line react/function-component-definition
const CustomTable: TableComponents['Table'] = ({ style, children }) => (
<TableStyled style={style}>{children}</TableStyled>
);
// eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents['TableRow'] = ({
children,
context,
...props
// eslint-disable-next-line react/jsx-props-no-spreading
}) => <TableRowStyled {...props}>{children}</TableRowStyled>;
function InfinityTable({
tableViewProps,
infitiyTableProps,
}: InfinityTableProps): JSX.Element | null {
const { onEndReached } = infitiyTableProps;
const { dataSource, columns } = useTableView(tableViewProps);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<>
{columns.map((column) => {
if (!column.render) return <td>Empty</td>;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
index,
);
const elementWithChildren = element as Exclude<
ColumnTypeRender<Record<string, unknown>>,
ReactNode
>;
const children = elementWithChildren.children as ReactElement;
const props = elementWithChildren.props as Record<string, unknown>;
return (
<TableCellStyled key={column.key}>
{cloneElement(children, props)}
</TableCellStyled>
);
})}
</>
),
[columns],
);
const tableHeader = useCallback(
() => (
<tr>
{columns.map((column) => (
<TableHeaderCellStyled key={column.key}>
{column.title as string}
</TableHeaderCellStyled>
))}
</tr>
),
[columns],
);
return (
<TableVirtuoso
style={infinityDefaultStyles}
data={dataSource}
components={{
Table: CustomTable,
// TODO: fix it in the future
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
TableRow: CustomTableRow,
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}
endReached={onEndReached}
totalCount={dataSource.length}
/>
);
}
export default InfinityTable;

View File

@ -0,0 +1,40 @@
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const TableStyled = styled.table`
width: 100%;
border-top: 1px solid rgba(253, 253, 253, 0.12);
border-radius: 2px 2px 0 0;
border-collapse: separate;
border-spacing: 0;
border-inline-start: 1px solid rgba(253, 253, 253, 0.12);
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
`;
export const TableCellStyled = styled.td`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
border-top: 1px solid rgba(253, 253, 253, 0.12);
background-color: ${themeColors.lightBlack};
`;
export const TableRowStyled = styled.tr`
&:hover {
${TableCellStyled} {
background-color: #1d1d1d;
}
}
`;
export const TableHeaderCellStyled = styled.th`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
background-color: #1d1d1d;
&:first-child {
border-start-start-radius: 2px;
}
&:last-child {
border-start-end-radius: 2px;
border-inline-end: none;
}
`;

View File

@ -0,0 +1,8 @@
import { LogsTableViewProps } from 'components/Logs/TableView/types';
export type InfinityTableProps = {
tableViewProps: LogsTableViewProps;
infitiyTableProps: {
onEndReached: (index: number) => void;
};
};

View File

@ -1,3 +1,11 @@
import { QueryDataV3 } from 'types/api/widgets/getQuery'; import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type LogsExplorerListProps = { data: QueryDataV3[]; isLoading: boolean }; export type LogsExplorerListProps = {
isLoading: boolean;
currentStagedQueryData: IBuilderQuery | null;
logs: ILog[];
onEndReached: (index: number) => void;
onExpand: (log: ILog) => void;
onOpenDetailedView: (log: ILog) => void;
};

View File

@ -2,38 +2,43 @@ import { Card, Typography } from 'antd';
// components // components
import ListLogView from 'components/Logs/ListLogView'; import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView'; import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { LogViewMode } from 'container/LogsTable'; import ExplorerControlPanel from 'container/ExplorerControlPanel';
import { Container, Heading } from 'container/LogsTable/styles'; import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu';
import { contentStyle } from 'container/Trace/Search/config'; import { contentStyle } from 'container/Trace/Search/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useFontFaceObserver from 'hooks/useFontObserver'; import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
// interfaces // interfaces
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import InfinityTableView from './InfinityTableView';
import { LogsExplorerListProps } from './LogsExplorerList.interfaces'; import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
import { InfinityWrapperStyled } from './styles';
import { convertKeysToColumnFields } from './utils';
function Footer(): JSX.Element {
return <Spinner height={20} tip="Getting Logs" />;
}
function LogsExplorerList({ function LogsExplorerList({
data,
isLoading, isLoading,
currentStagedQueryData,
logs,
onOpenDetailedView,
onEndReached,
onExpand,
}: LogsExplorerListProps): JSX.Element { }: LogsExplorerListProps): JSX.Element {
const [viewMode] = useState<LogViewMode>('raw'); const { initialDataSource } = useQueryBuilder();
const [linesPerRow] = useState<number>(20);
const logs: ILog[] = useMemo(() => { const { options, config } = useOptionsMenu({
if (data.length > 0 && data[0].list) { dataSource: initialDataSource || DataSource.METRICS,
const logs: ILog[] = data[0].list.map((item) => ({ aggregateOperator:
timestamp: +item.timestamp, currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
...item.data, });
}));
return logs;
}
return [];
}, [data]);
useFontFaceObserver( useFontFaceObserver(
[ [
@ -42,75 +47,107 @@ function LogsExplorerList({
weight: '300', weight: '300',
}, },
], ],
viewMode === 'raw', options.format === 'raw',
{ {
timeout: 5000, timeout: 5000,
}, },
); );
// TODO: implement here linesPerRow, mode like in useSelectedLogView
const selectedFields = useMemo(
() => convertKeysToColumnFields(options.selectColumns),
[options],
);
const getItemContent = useCallback( const getItemContent = useCallback(
(index: number): JSX.Element => { (_: number, log: ILog): JSX.Element => {
const log = logs[index]; if (options.format === 'raw') {
if (viewMode === 'raw') {
return ( return (
<RawLogView <RawLogView
key={log.id} key={log.id}
data={log} data={log}
linesPerRow={linesPerRow} linesPerRow={options.maxLines}
// TODO: write new onClickExpanded logic onClickExpand={onExpand}
onClickExpand={(): void => {}}
/> />
); );
} }
return <ListLogView key={log.id} logData={log} />; return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
onOpenDetailedView={onOpenDetailedView}
/>
);
}, },
[logs, linesPerRow, viewMode], [
options.format,
options.maxLines,
selectedFields,
onOpenDetailedView,
onExpand,
],
); );
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
if (viewMode === 'table') { const components = isLoading
? {
Footer,
}
: {};
if (options.format === 'table') {
return ( return (
<LogsTableView <InfinityTableView
logs={logs} tableViewProps={{
// TODO: write new selected logic logs,
fields={[]} fields: selectedFields,
linesPerRow={linesPerRow} linesPerRow: options.maxLines,
// TODO: write new onClickExpanded logic onClickExpand: onExpand,
onClickExpand={(): void => {}} }}
infitiyTableProps={{ onEndReached }}
/> />
); );
} }
return ( return (
<Card bodyStyle={contentStyle}> <Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
<Virtuoso <Virtuoso
useWindowScroll useWindowScroll
data={logs}
endReached={onEndReached}
totalCount={logs.length} totalCount={logs.length}
itemContent={getItemContent} itemContent={getItemContent}
components={components}
/> />
</Card> </Card>
); );
}, [getItemContent, linesPerRow, logs, viewMode]); }, [
isLoading,
if (isLoading) { logs,
return <Spinner height={20} tip="Getting Logs" />; options.format,
} options.maxLines,
onEndReached,
getItemContent,
selectedFields,
onExpand,
]);
return ( return (
<Container> <>
{viewMode !== 'table' && ( <ExplorerControlPanel
isLoading={isLoading}
isShowPageSize={false}
optionsMenuConfig={config}
/>
{options.format !== 'table' && (
<Heading> <Heading>
<Typography.Text>Event</Typography.Text> <Typography.Text>Event</Typography.Text>
</Heading> </Heading>
)} )}
{logs.length === 0 && <Typography>No logs lines found</Typography>} {logs.length === 0 && <Typography>No logs lines found</Typography>}
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
{renderContent} </>
</Container>
); );
} }

View File

@ -0,0 +1,6 @@
import styled from 'styled-components';
export const InfinityWrapperStyled = styled.div`
min-height: 40rem;
display: flex;
`;

View File

@ -0,0 +1,11 @@
import { IField } from 'types/api/logs/fields';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const convertKeysToColumnFields = (
keys: BaseAutocompleteData[],
): IField[] =>
keys.map((item) => ({
dataType: item.dataType as string,
name: item.key,
type: item.type as string,
}));

View File

@ -6,8 +6,8 @@ import { memo } from 'react';
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces'; import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
function LogsExplorerTable({ function LogsExplorerTable({
isLoading,
data, data,
isLoading,
}: LogsExplorerTableProps): JSX.Element { }: LogsExplorerTableProps): JSX.Element {
const { stagedQuery } = useQueryBuilder(); const { stagedQuery } = useQueryBuilder();

View File

@ -1,50 +1,62 @@
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import LogExplorerDetailedView from 'container/LogExplorerDetailedView';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList'; import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable'; import LogsExplorerTable from 'container/LogsExplorerTable';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useCallback, useEffect, useMemo } from 'react'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { useSelector } from 'react-redux'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { AppState } from 'store/reducers'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder'; import { ILog } from 'types/api/logs/log';
import { GlobalReducer } from 'types/reducer/globalTime'; import {
IBuilderQuery,
OrderByPayload,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { TabsStyled } from './LogsExplorerViews.styled'; import { TabsStyled } from './LogsExplorerViews.styled';
function LogsExplorerViews(): JSX.Element { function LogsExplorerViews(): JSX.Element {
const { queryData: pageSize } = useUrlQueryData(
queryParamNamesMap.pageSize,
DEFAULT_PER_PAGE_VALUE,
);
// Context
const { const {
currentQuery, currentQuery,
stagedQuery, stagedQuery,
panelType, panelType,
isEnabledQuery,
updateAllQueriesOperators, updateAllQueriesOperators,
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
} = useQueryBuilder(); } = useQueryBuilder();
const { selectedTime } = useSelector<AppState, GlobalReducer>( // State
(state) => state.globalTime, const [activeLog, setActiveLog] = useState<ILog | null>(null);
); const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const { data, isFetching, isError } = useGetQueryRange( const currentStagedQueryData = useMemo(() => {
{ if (!stagedQuery || stagedQuery.builder.queryData.length !== 1) return null;
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelType || PANEL_TYPES.LIST, return stagedQuery.builder.queryData[0];
globalSelectedInterval: selectedTime, }, [stagedQuery]);
selectedTime: 'GLOBAL_TIME',
params: { const orderByTimestamp: OrderByPayload | null = useMemo(() => {
dataSource: DataSource.LOGS, const timestampOrderBy = currentStagedQueryData?.orderBy.find(
}, (item) => item.columnName === 'timestamp',
}, );
{
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery], return timestampOrderBy || null;
enabled: isEnabledQuery, }, [currentStagedQueryData]);
},
);
const isMultipleQueries = useMemo( const isMultipleQueries = useMemo(
() => () =>
@ -62,35 +74,57 @@ function LogsExplorerViews(): JSX.Element {
return groupByCount > 0; return groupByCount > 0;
}, [currentQuery]); }, [currentQuery]);
const currentData = useMemo( const isLimit: boolean = useMemo(() => {
() => data?.payload.data.newResult.data.result || [], if (!currentStagedQueryData) return false;
[data], if (!currentStagedQueryData.limit) return false;
return logs.length >= currentStagedQueryData.limit;
}, [logs.length, currentStagedQueryData]);
const listChartQuery = useMemo(() => {
if (!stagedQuery || !currentStagedQueryData) return null;
const modifiedQueryData: IBuilderQuery = {
...currentStagedQueryData,
aggregateOperator: StringOperators.COUNT,
};
const modifiedQuery: Query = {
...stagedQuery,
builder: {
...stagedQuery.builder,
queryData: stagedQuery.builder.queryData.map((item) => ({
...item,
...modifiedQueryData,
})),
},
};
return modifiedQuery;
}, [stagedQuery, currentStagedQueryData]);
const listChartData = useGetExplorerQueryRange(
listChartQuery,
PANEL_TYPES.TIME_SERIES,
); );
const tabsItems: TabsProps['items'] = useMemo( const { data, isFetching, isError } = useGetExplorerQueryRange(
() => [ requestData,
{ panelType,
label: 'List View', {
key: PANEL_TYPES.LIST, keepPreviousData: true,
disabled: isMultipleQueries || isGroupByExist, enabled: !isLimit,
children: <LogsExplorerList data={currentData} isLoading={isFetching} />, },
},
{
label: 'TimeSeries',
key: PANEL_TYPES.TIME_SERIES,
children: (
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
),
},
{
label: 'Table',
key: PANEL_TYPES.TABLE,
children: <LogsExplorerTable data={currentData} isLoading={isFetching} />,
},
],
[isMultipleQueries, isGroupByExist, currentData, isFetching, data, isError],
); );
const handleSetActiveLog = useCallback((nextActiveLog: ILog) => {
setActiveLog(nextActiveLog);
}, []);
const handleClearActiveLog = useCallback(() => {
setActiveLog(null);
}, []);
const handleChangeView = useCallback( const handleChangeView = useCallback(
(newPanelType: string) => { (newPanelType: string) => {
if (newPanelType === panelType) return; if (newPanelType === panelType) return;
@ -101,7 +135,9 @@ function LogsExplorerViews(): JSX.Element {
DataSource.LOGS, DataSource.LOGS,
); );
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType }); redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
}, },
[ [
currentQuery, currentQuery,
@ -111,6 +147,75 @@ function LogsExplorerViews(): JSX.Element {
], ],
); );
const getRequestData = useCallback(
(
query: Query | null,
params: { page: number; log: ILog | null; pageSize: number },
): Query | null => {
if (!query) return null;
const paginateData = getPaginationQueryData({
currentStagedQueryData,
listItemId: params.log ? params.log.id : null,
orderByTimestamp,
page: params.page,
pageSize: params.pageSize,
});
const data: Query = {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData.map((item) => ({
...item,
...paginateData,
pageSize: params.pageSize,
})),
},
};
return data;
},
[currentStagedQueryData, orderByTimestamp],
);
const handleEndReached = useCallback(
(index: number) => {
if (isLimit) return;
const lastLog = logs[index];
const limit = currentStagedQueryData?.limit;
const nextLogsLenth = logs.length + pageSize;
const nextPageSize =
limit && nextLogsLenth >= limit ? limit - logs.length : pageSize;
if (!stagedQuery) return;
const newRequestData = getRequestData(stagedQuery, {
page: page + 1,
log: orderByTimestamp ? lastLog : null,
pageSize: nextPageSize,
});
setPage((prevPage) => prevPage + 1);
setRequestData(newRequestData);
},
[
isLimit,
logs,
currentStagedQueryData?.limit,
pageSize,
stagedQuery,
getRequestData,
page,
orderByTimestamp,
],
);
useEffect(() => { useEffect(() => {
const shouldChangeView = isMultipleQueries || isGroupByExist; const shouldChangeView = isMultipleQueries || isGroupByExist;
@ -119,15 +224,115 @@ function LogsExplorerViews(): JSX.Element {
} }
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]); }, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
useEffect(() => {
const currentData = data?.payload.data.newResult.data.result || [];
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
}
}, [data]);
useEffect(() => {
if (requestData?.id !== stagedQuery?.id) {
const newRequestData = getRequestData(stagedQuery, {
page: 1,
log: null,
pageSize,
});
setLogs([]);
setPage(1);
setRequestData(newRequestData);
}
}, [stagedQuery, requestData, getRequestData, pageSize]);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: 'List View',
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries || isGroupByExist,
children: (
<LogsExplorerList
isLoading={isFetching}
currentStagedQueryData={currentStagedQueryData}
logs={logs}
onOpenDetailedView={handleSetActiveLog}
onEndReached={handleEndReached}
onExpand={handleSetActiveLog}
/>
),
},
{
label: 'TimeSeries',
key: PANEL_TYPES.TIME_SERIES,
children: (
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
),
},
{
label: 'Table',
key: PANEL_TYPES.TABLE,
children: (
<LogsExplorerTable
data={data?.payload.data.newResult.data.result || []}
isLoading={isFetching}
/>
),
},
],
[
isMultipleQueries,
isGroupByExist,
isFetching,
currentStagedQueryData,
logs,
handleSetActiveLog,
handleEndReached,
data,
isError,
],
);
const chartData = useMemo(() => {
if (!stagedQuery) return [];
if (panelType === PANEL_TYPES.LIST) {
if (
listChartData &&
listChartData.data &&
listChartData.data.payload.data.result.length > 0
) {
return listChartData.data.payload.data.result;
}
return [];
}
if (!data || data.payload.data.result.length === 0) return [];
const isGroupByExist = stagedQuery.builder.queryData.some(
(queryData) => queryData.groupBy.length > 0,
);
return isGroupByExist
? data.payload.data.result
: [data.payload.data.result[0]];
}, [stagedQuery, data, panelType, listChartData]);
return ( return (
<div> <>
<LogsExplorerChart isLoading={isFetching} data={chartData} />
<TabsStyled <TabsStyled
items={tabsItems} items={tabsItems}
defaultActiveKey={panelType || PANEL_TYPES.LIST} defaultActiveKey={panelType || PANEL_TYPES.LIST}
activeKey={panelType || PANEL_TYPES.LIST} activeKey={panelType || PANEL_TYPES.LIST}
onChange={handleChangeView} onChange={handleChangeView}
destroyInactiveTabPane
/> />
</div> <LogExplorerDetailedView log={activeLog} onClose={handleClearActiveLog} />
</>
); );
} }

View File

@ -7,10 +7,11 @@ import Spinner from 'components/Spinner';
import { contentStyle } from 'container/Trace/Search/config'; import { contentStyle } from 'container/Trace/Search/config';
import useFontFaceObserver from 'hooks/useFontObserver'; import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
// interfaces
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
// interfaces
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
@ -28,6 +29,8 @@ type LogsTableProps = {
function LogsTable(props: LogsTableProps): JSX.Element { function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, onClickExpand, linesPerRow } = props; const { viewMode, onClickExpand, linesPerRow } = props;
const dispatch = useDispatch();
useFontFaceObserver( useFontFaceObserver(
[ [
{ {
@ -58,6 +61,16 @@ function LogsTable(props: LogsTableProps): JSX.Element {
liveTail, liveTail,
]); ]);
const handleOpenDetailedView = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const getItemContent = useCallback( const getItemContent = useCallback(
(index: number): JSX.Element => { (index: number): JSX.Element => {
const log = logs[index]; const log = logs[index];
@ -73,9 +86,23 @@ function LogsTable(props: LogsTableProps): JSX.Element {
); );
} }
return <ListLogView key={log.id} logData={log} />; return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selected}
onOpenDetailedView={handleOpenDetailedView}
/>
);
}, },
[logs, linesPerRow, viewMode, onClickExpand], [
logs,
viewMode,
selected,
handleOpenDetailedView,
linesPerRow,
onClickExpand,
],
); );
const renderContent = useMemo(() => { const renderContent = useMemo(() => {

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { initialQueriesMap } from 'constants/queryBuilder'; import { initialQueriesMap } from 'constants/queryBuilder';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
history.push( history.push(
`${history.location.pathname}/new?graphType=${name}&widgetId=${ `${history.location.pathname}/new?graphType=${name}&widgetId=${
emptyLayout.i emptyLayout.i
}&${COMPOSITE_QUERY}=${encodeURIComponent( }&${queryParamNamesMap.compositeQuery}=${encodeURIComponent(
JSON.stringify(initialQueriesMap.metrics), JSON.stringify(initialQueriesMap.metrics),
)}`, )}`,
); );

View File

@ -18,9 +18,9 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null {
value={config.value} value={config.value}
onChange={config.onChange} onChange={config.onChange}
> >
<RadioButton value="row">{t('options_menu.row')}</RadioButton> <RadioButton value="raw">{t('options_menu.row')}</RadioButton>
<RadioButton value="default">{t('options_menu.default')}</RadioButton> <RadioButton value="list">{t('options_menu.default')}</RadioButton>
<RadioButton value="column">{t('options_menu.column')}</RadioButton> <RadioButton value="table">{t('options_menu.column')}</RadioButton>
</RadioGroup> </RadioGroup>
</FormatFieldWrapper> </FormatFieldWrapper>
); );

View File

@ -4,6 +4,6 @@ export const URL_OPTIONS = 'options';
export const defaultOptionsQuery: OptionsQuery = { export const defaultOptionsQuery: OptionsQuery = {
selectColumns: [], selectColumns: [],
maxLines: 0, maxLines: 2,
format: 'default', format: 'list',
}; };

View File

@ -1,10 +1,11 @@
import { InputNumberProps, RadioProps, SelectProps } from 'antd'; import { InputNumberProps, RadioProps, SelectProps } from 'antd';
import { LogViewMode } from 'container/LogsTable';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export interface OptionsQuery { export interface OptionsQuery {
selectColumns: BaseAutocompleteData[]; selectColumns: BaseAutocompleteData[];
maxLines: number; maxLines: number;
format: 'default' | 'row' | 'column'; format: LogViewMode;
} }
export interface InitialOptions export interface InitialOptions

View File

@ -35,16 +35,17 @@ const useOptionsMenu = ({
query: optionsQuery, query: optionsQuery,
queryData: optionsQueryData, queryData: optionsQueryData,
redirectWithQuery: redirectWithOptionsData, redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS); } = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
const { data, isFetched, isLoading } = useQuery( const { data, isFetched, isLoading } = useQuery(
[QueryBuilderKeys.GET_ATTRIBUTE_KEY], [QueryBuilderKeys.GET_ATTRIBUTE_KEY, dataSource, aggregateOperator],
async () => async () =>
getAggregateKeys({ getAggregateKeys({
searchText: '', searchText: '',
dataSource, dataSource,
aggregateOperator, aggregateOperator,
aggregateAttribute: '', aggregateAttribute: '',
tagType: null,
}), }),
); );
@ -86,11 +87,16 @@ const useOptionsMenu = ({
}, [] as BaseAutocompleteData[]); }, [] as BaseAutocompleteData[]);
redirectWithOptionsData({ redirectWithOptionsData({
...defaultOptionsQuery, ...optionsQueryData,
selectColumns: newSelectedColumns, selectColumns: newSelectedColumns,
}); });
}, },
[attributeKeys, selectedColumnKeys, redirectWithOptionsData], [
selectedColumnKeys,
redirectWithOptionsData,
optionsQueryData,
attributeKeys,
],
); );
const handleRemoveSelectedColumn = useCallback( const handleRemoveSelectedColumn = useCallback(
@ -116,21 +122,21 @@ const useOptionsMenu = ({
const handleFormatChange = useCallback( const handleFormatChange = useCallback(
(event: RadioChangeEvent) => { (event: RadioChangeEvent) => {
redirectWithOptionsData({ redirectWithOptionsData({
...defaultOptionsQuery, ...optionsQueryData,
format: event.target.value, format: event.target.value,
}); });
}, },
[redirectWithOptionsData], [optionsQueryData, redirectWithOptionsData],
); );
const handleMaxLinesChange = useCallback( const handleMaxLinesChange = useCallback(
(value: string | number | null) => { (value: string | number | null) => {
redirectWithOptionsData({ redirectWithOptionsData({
...defaultOptionsQuery, ...optionsQueryData,
maxLines: value as number, maxLines: value as number,
}); });
}, },
[redirectWithOptionsData], [optionsQueryData, redirectWithOptionsData],
); );
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo( const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(

View File

@ -0,0 +1,4 @@
export type PageSizeSelectProps = {
isLoading: boolean;
isShow: boolean;
};

View File

@ -0,0 +1,51 @@
import { Col, Row, Select } from 'antd';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import {
defaultSelectStyle,
ITEMS_PER_PAGE_OPTIONS,
} from 'container/Controls/config';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback } from 'react';
import { PageSizeSelectProps } from './PageSizeSelect.interfaces';
function PageSizeSelect({
isLoading,
isShow,
}: PageSizeSelectProps): JSX.Element | null {
const { redirectWithQuery, queryData: pageSize } = useUrlQueryData<number>(
queryParamNamesMap.pageSize,
ITEMS_PER_PAGE_OPTIONS[0],
);
const handleChangePageSize = useCallback(
(value: number) => {
redirectWithQuery(value);
},
[redirectWithQuery],
);
if (!isShow) return null;
return (
<Row>
<Col>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={pageSize}
onChange={handleChangePageSize}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Col>
</Row>
);
}
export default PageSizeSelect;

View File

@ -1,5 +1,6 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
export type QueryBuilderConfig = export type QueryBuilderConfig =
@ -13,4 +14,5 @@ export type QueryBuilderProps = {
config?: QueryBuilderConfig; config?: QueryBuilderConfig;
panelType: ITEMS; panelType: ITEMS;
actions?: ReactNode; actions?: ReactNode;
inactiveFilters?: Partial<Record<keyof IBuilderQuery, boolean>>;
}; };

View File

@ -17,6 +17,7 @@ export const QueryBuilder = memo(function QueryBuilder({
config, config,
panelType: newPanelType, panelType: newPanelType,
actions, actions,
inactiveFilters = {},
}: QueryBuilderProps): JSX.Element { }: QueryBuilderProps): JSX.Element {
const { const {
currentQuery, currentQuery,
@ -74,6 +75,7 @@ export const QueryBuilder = memo(function QueryBuilder({
isAvailableToDisable={isAvailableToDisableQuery} isAvailableToDisable={isAvailableToDisableQuery}
queryVariant={config?.queryVariant || 'dropdown'} queryVariant={config?.queryVariant || 'dropdown'}
query={query} query={query}
inactiveFilters={inactiveFilters}
/> />
</Col> </Col>
))} ))}

View File

@ -1,3 +1,4 @@
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type QueryProps = { export type QueryProps = {
@ -5,4 +6,4 @@ export type QueryProps = {
isAvailableToDisable: boolean; isAvailableToDisable: boolean;
query: IBuilderQuery; query: IBuilderQuery;
queryVariant: 'static' | 'dropdown'; queryVariant: 'static' | 'dropdown';
}; } & Pick<QueryBuilderProps, 'inactiveFilters'>;

View File

@ -35,6 +35,7 @@ export const Query = memo(function Query({
isAvailableToDisable, isAvailableToDisable,
queryVariant, queryVariant,
query, query,
inactiveFilters,
}: QueryProps): JSX.Element { }: QueryProps): JSX.Element {
const { panelType } = useQueryBuilder(); const { panelType } = useQueryBuilder();
const { const {
@ -47,7 +48,7 @@ export const Query = memo(function Query({
handleChangeQueryData, handleChangeQueryData,
handleChangeOperator, handleChangeOperator,
handleDeleteQuery, handleDeleteQuery,
} = useQueryOperations({ index, query }); } = useQueryOperations({ index, query, inactiveFilters });
const handleChangeAggregateEvery = useCallback( const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => { (value: IBuilderQuery['stepInterval']) => {
@ -109,6 +110,24 @@ export const Query = memo(function Query({
[handleChangeQueryData], [handleChangeQueryData],
); );
const renderAggregateEveryFilter = useCallback(
(): JSX.Element | null =>
!inactiveFilters?.stepInterval ? (
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
) : null,
[inactiveFilters?.stepInterval, query, handleChangeAggregateEvery],
);
const renderAdditionalFilters = useCallback((): ReactNode => { const renderAdditionalFilters = useCallback((): ReactNode => {
switch (panelType) { switch (panelType) {
case PANEL_TYPES.TIME_SERIES: { case PANEL_TYPES.TIME_SERIES: {
@ -149,19 +168,7 @@ export const Query = memo(function Query({
</Col> </Col>
)} )}
<Col span={11}> <Col span={11}>{renderAggregateEveryFilter()}</Col>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
</> </>
); );
} }
@ -179,19 +186,7 @@ export const Query = memo(function Query({
</Col> </Col>
</Row> </Row>
</Col> </Col>
<Col span={11}> <Col span={11}>{renderAggregateEveryFilter()}</Col>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
</> </>
); );
} }
@ -230,21 +225,7 @@ export const Query = memo(function Query({
</Row> </Row>
</Col> </Col>
{panelType !== PANEL_TYPES.LIST && ( <Col span={11}>{renderAggregateEveryFilter()}</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
)}
</> </>
); );
} }
@ -253,10 +234,10 @@ export const Query = memo(function Query({
panelType, panelType,
query, query,
isMetricsDataSource, isMetricsDataSource,
handleChangeAggregateEvery,
handleChangeHavingFilter, handleChangeHavingFilter,
handleChangeLimit, handleChangeLimit,
handleChangeOrderByKeys, handleChangeOrderByKeys,
renderAggregateEveryFilter,
]); ]);
return ( return (

View File

@ -27,7 +27,7 @@ export function OrderByFilter({
}: OrderByFilterProps): JSX.Element { }: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [selectedValue, setSelectedValue] = useState<IOption[]>( const [selectedValue, setSelectedValue] = useState<IOption[]>(
transformToOrderByStringValues(query.orderBy) || [], transformToOrderByStringValues(query.orderBy),
); );
const { data, isFetching } = useQuery( const { data, isFetching } = useQuery(

View File

@ -8,11 +8,25 @@ export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = ( export const transformToOrderByStringValues = (
orderBy: OrderByPayload[], orderBy: OrderByPayload[],
): IOption[] => ): IOption[] => {
orderBy.map((item) => ({ const prepareSelectedValue: IOption[] = orderBy.reduce<IOption[]>(
label: `${item.columnName} ${item.order}`, (acc, item) => {
value: `${item.columnName}${orderByValueDelimiter}${item.order}`, if (item.columnName === '#SIGNOZ_VALUE') return acc;
}));
const option: IOption = {
label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
acc.push(option);
return acc;
},
[],
);
return prepareSelectedValue;
};
export function mapLabelValuePairs( export function mapLabelValuePairs(
arr: BaseAutocompleteData[], arr: BaseAutocompleteData[],

View File

@ -1,4 +1,4 @@
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -7,7 +7,7 @@ export const useGetCompositeQueryParam = (): Query | null => {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
return useMemo(() => { return useMemo(() => {
const compositeQuery = urlQuery.get(COMPOSITE_QUERY); const compositeQuery = urlQuery.get(queryParamNamesMap.compositeQuery);
let parsedCompositeQuery: Query | null = null; let parsedCompositeQuery: Query | null = null;
try { try {

View File

@ -0,0 +1,58 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useMemo } from 'react';
import { UseQueryOptions, UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { useGetQueryRange } from './useGetQueryRange';
import { useQueryBuilder } from './useQueryBuilder';
export const useGetExplorerQueryRange = (
requestData: Query | null,
panelType: GRAPH_TYPES | null,
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
const { isEnabledQuery } = useQueryBuilder();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const key = useMemo(
() =>
typeof options?.queryKey === 'string'
? options?.queryKey
: REACT_QUERY_KEY.GET_QUERY_RANGE,
[options?.queryKey],
);
const isEnabled = useMemo(() => {
if (!options) return isEnabledQuery;
if (typeof options.enabled === 'boolean') {
return isEnabledQuery && options.enabled;
}
return isEnabledQuery;
}, [options, isEnabledQuery]);
return useGetQueryRange(
{
graphType: panelType || PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval,
query: requestData || initialQueriesMap.metrics,
},
{
...options,
retry: false,
queryKey: [key, globalSelectedInterval, requestData],
enabled: isEnabled,
},
);
};

View File

@ -1,4 +1,4 @@
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -9,7 +9,7 @@ export const useGetPanelTypesQueryParam = <T extends GRAPH_TYPES | undefined>(
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
return useMemo(() => { return useMemo(() => {
const panelTypeQuery = urlQuery.get(PANEL_TYPES_QUERY); const panelTypeQuery = urlQuery.get(queryParamNamesMap.panelTypes);
return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType; return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType;
}, [urlQuery, defaultPanelType]); }, [urlQuery, defaultPanelType]);

View File

@ -17,7 +17,11 @@ import {
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select'; import { SelectOption } from 'types/common/select';
export const useQueryOperations: UseQueryOperations = ({ query, index }) => { export const useQueryOperations: UseQueryOperations = ({
query,
index,
inactiveFilters,
}) => {
const { const {
handleSetQueryData, handleSetQueryData,
removeQueryBuilderEntityByIndex, removeQueryBuilderEntityByIndex,
@ -58,15 +62,23 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
const getNewListOfAdditionalFilters = useCallback( const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource): string[] => { (dataSource: DataSource): string[] => {
const listOfFilters = mapOfFilters[dataSource].map((item) => item.text); const result: string[] = mapOfFilters[dataSource].reduce<string[]>(
(acc, item) => {
if (inactiveFilters && inactiveFilters[item.field]) {
return acc;
}
if (panelType === PANEL_TYPES.LIST) { acc.push(item.text);
return listOfFilters.filter((filter) => filter !== 'Aggregation interval');
}
return listOfFilters; return acc;
},
[],
);
return result;
}, },
[panelType],
[inactiveFilters],
); );
const handleChangeAggregatorAttribute = useCallback( const handleChangeAggregatorAttribute = useCallback(

View File

@ -1,46 +0,0 @@
import { ChartData } from 'chart.js';
import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery';
import { colors } from '../getRandomColor';
export const getExplorerChartData = (
queryData: QueryData[],
): ChartData<'bar'> => {
const uniqueTimeLabels = new Set<number>();
const sortedData = [...queryData].sort((a, b) => {
if (a.queryName < b.queryName) return -1;
if (a.queryName > b.queryName) return 1;
return 0;
});
const modifiedData: { label: string }[] = sortedData.map((result) => {
const { metric, queryName, legend } = result;
result.values.forEach((value) => {
uniqueTimeLabels.add(value[0] * 1000);
});
return {
label: getLabelName(metric, queryName || '', legend || ''),
};
});
const labels = Array.from(uniqueTimeLabels)
.sort((a, b) => a - b)
.map((value) => new Date(value));
const allLabels = modifiedData.map((e) => e.label);
const data: ChartData<'bar'> = {
labels,
datasets: queryData.map((result, index) => ({
label: allLabels[index],
data: result.values.map((item) => parseFloat(item[1])),
backgroundColor: colors[index % colors.length] || 'red',
borderColor: colors[index % colors.length] || 'red',
})),
};
return data;
};

View File

@ -1,11 +1,14 @@
import { ChartData } from 'chart.js'; import { ChartData, ChartDataset } from 'chart.js';
import getLabelName from 'lib/getLabelName'; import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery'; import { QueryData } from 'types/api/widgets/getQuery';
import convertIntoEpoc from './covertIntoEpoc'; import convertIntoEpoc from './covertIntoEpoc';
import { colors } from './getRandomColor'; import { colors } from './getRandomColor';
const getChartData = ({ queryData }: GetChartDataProps): ChartData => { const getChartData = ({
queryData,
createDataset,
}: GetChartDataProps): ChartData => {
const uniqueTimeLabels = new Set<number>(); const uniqueTimeLabels = new Set<number>();
queryData.forEach((data) => { queryData.forEach((data) => {
data.queryData.forEach((query) => { data.queryData.forEach((query) => {
@ -60,28 +63,39 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
.reduce((a, b) => [...a, ...b], []); .reduce((a, b) => [...a, ...b], []);
return { return {
datasets: alldata.map((e, index) => ({ datasets: alldata.map((e, index) => {
data: e, const datasetBaseConfig = {
label: allLabels[index], label: allLabels[index],
borderWidth: 1.5, borderColor: colors[index % colors.length] || 'red',
spanGaps: true, data: e,
animations: false, borderWidth: 1.5,
borderColor: colors[index % colors.length] || 'red', spanGaps: true,
showLine: true, animations: false,
pointRadius: 0, showLine: true,
})), pointRadius: 0,
};
return createDataset
? createDataset(e, index, allLabels)
: datasetBaseConfig;
}),
labels: response labels: response
.map((e) => e.map((e) => e.first)) .map((e) => e.map((e) => e.first))
.reduce((a, b) => [...a, ...b], [])[0], .reduce((a, b) => [...a, ...b], [])[0],
}; };
}; };
interface GetChartDataProps { export interface GetChartDataProps {
queryData: { queryData: {
query?: string; query?: string;
legend?: string; legend?: string;
queryData: QueryData[]; queryData: QueryData[];
}[]; }[];
createDataset?: (
element: (number | null)[],
index: number,
allLabels: string[],
) => ChartDataset;
} }
export default getChartData; export default getChartData;

View File

@ -0,0 +1,76 @@
import { initialFilters } from 'constants/queryBuilder';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import {
IBuilderQuery,
OrderByPayload,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
type SetupPaginationQueryDataParams = {
currentStagedQueryData: IBuilderQuery | null;
listItemId: string | null;
orderByTimestamp: OrderByPayload | null;
page: number;
pageSize: number;
};
type SetupPaginationQueryData = (
params: SetupPaginationQueryDataParams,
) => Pick<IBuilderQuery, 'filters' | 'offset'>;
export const getPaginationQueryData: SetupPaginationQueryData = ({
currentStagedQueryData,
listItemId,
orderByTimestamp,
page,
pageSize,
}) => {
if (!currentStagedQueryData) {
return { limit: null, filters: initialFilters };
}
const filters = currentStagedQueryData.filters || initialFilters;
const offset = (page - 1) * pageSize;
const queryProps =
(orderByTimestamp && currentStagedQueryData.orderBy.length > 1) ||
!orderByTimestamp
? {
offset,
}
: {};
const updatedFilters: TagFilter = {
...filters,
items: filters.items.filter((item) => item.key?.key !== 'id'),
};
const tagFilters: TagFilter = {
...filters,
items:
listItemId && orderByTimestamp
? [
{
id: uuid(),
key: {
key: 'id',
type: null,
dataType: 'string',
isColumn: true,
},
op: orderByTimestamp.order === FILTERS.ASC ? '>' : '<',
value: listItemId,
},
...updatedFilters.items,
]
: updatedFilters.items,
};
const chunkOfQueryData: Partial<IBuilderQuery> = {
filters: orderByTimestamp ? tagFilters : updatedFilters,
...queryProps,
};
return { ...currentStagedQueryData, ...chunkOfQueryData };
};

View File

@ -39,6 +39,7 @@ type CreateTableDataFromQuery = (
type FillColumnData = ( type FillColumnData = (
queryTableData: QueryDataV3[], queryTableData: QueryDataV3[],
dynamicColumns: DynamicColumns, dynamicColumns: DynamicColumns,
query: Query,
) => { filledDynamicColumns: DynamicColumns; rowsLength: number }; ) => { filledDynamicColumns: DynamicColumns; rowsLength: number };
type GetDynamicColumns = ( type GetDynamicColumns = (
@ -177,7 +178,8 @@ const fillEmptyRowCells = (
const fillDataFromSeria = ( const fillDataFromSeria = (
seria: SeriesItem, seria: SeriesItem,
columns: DynamicColumns, columns: DynamicColumns,
currentQueryName: string, queryName: string,
operator: string,
): void => { ): void => {
const labelEntries = Object.entries(seria.labels); const labelEntries = Object.entries(seria.labels);
@ -193,7 +195,13 @@ const fillDataFromSeria = (
return; return;
} }
if (currentQueryName === column.key) { if (isFormula(queryName) && queryName === column.key) {
column.data.push(parseFloat(value.value).toFixed(2));
unusedColumnsKeys.delete(column.key);
return;
}
if (!isFormula(queryName) && operator === column.key) {
column.data.push(parseFloat(value.value).toFixed(2)); column.data.push(parseFloat(value.value).toFixed(2));
unusedColumnsKeys.delete(column.key); unusedColumnsKeys.delete(column.key);
return; return;
@ -230,20 +238,25 @@ const fillDataFromList = (
}); });
}; };
const fillColumnsData: FillColumnData = (queryTableData, cols) => { const fillColumnsData: FillColumnData = (queryTableData, cols, query) => {
const fields = cols.filter((item) => item.type === 'field'); const fields = cols.filter((item) => item.type === 'field');
const operators = cols.filter((item) => item.type === 'operator'); const operators = cols.filter((item) => item.type === 'operator');
const resultColumns = [...fields, ...operators]; const resultColumns = [...fields, ...operators];
queryTableData.forEach((currentQuery) => { queryTableData.forEach((currentQuery) => {
// const currentOperator = getQueryOperator(
// query.builder.queryData,
// currentQuery.queryName,
// );
if (currentQuery.series) { if (currentQuery.series) {
currentQuery.series.forEach((seria) => { currentQuery.series.forEach((seria) => {
fillDataFromSeria(seria, resultColumns, currentQuery.queryName); const currentOperator = getQueryOperator(
query.builder.queryData,
currentQuery.queryName,
);
fillDataFromSeria(
seria,
resultColumns,
currentQuery.queryName,
currentOperator,
);
}); });
} }
@ -313,6 +326,7 @@ export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({
const { filledDynamicColumns, rowsLength } = fillColumnsData( const { filledDynamicColumns, rowsLength } = fillColumnsData(
queryTableData, queryTableData,
dynamicColumns, dynamicColumns,
query,
); );
const dataSource = generateData(filledDynamicColumns, rowsLength); const dataSource = generateData(filledDynamicColumns, rowsLength);

View File

@ -1,8 +1,8 @@
import { Button, Col, Row } from 'antd'; import { Button, Col, Row } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerViews from 'container/LogsExplorerViews'; import LogsExplorerViews from 'container/LogsExplorerViews';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
@ -11,23 +11,33 @@ import { DataSource } from 'types/common/queryBuilder';
// ** Styles // ** Styles
import { ButtonWrapperStyled, WrapperStyled } from './styles'; import { ButtonWrapperStyled, WrapperStyled } from './styles';
import { prepareQueryWithDefaultTimestamp } from './utils';
function LogsExplorer(): JSX.Element { function LogsExplorer(): JSX.Element {
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder(); const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const defaultValue = useMemo( const defaultValue = useMemo(() => {
() => const updatedQuery = updateAllQueriesOperators(
updateAllQueriesOperators( initialQueriesMap.logs,
initialQueriesMap.logs, PANEL_TYPES.LIST,
PANEL_TYPES.LIST, DataSource.LOGS,
DataSource.LOGS, );
), return prepareQueryWithDefaultTimestamp(updatedQuery);
[updateAllQueriesOperators], }, [updateAllQueriesOperators]);
);
useShareBuilderUrl(defaultValue); useShareBuilderUrl(defaultValue);
const inactiveLogsFilters: QueryBuilderProps['inactiveFilters'] = useMemo(() => {
if (panelTypes === PANEL_TYPES.TABLE) {
const result: QueryBuilderProps['inactiveFilters'] = { stepInterval: true };
return result;
}
return {};
}, [panelTypes]);
return ( return (
<WrapperStyled> <WrapperStyled>
<Row gutter={[0, 28]}> <Row gutter={[0, 28]}>
@ -35,6 +45,7 @@ function LogsExplorer(): JSX.Element {
<QueryBuilder <QueryBuilder
panelType={panelTypes} panelType={panelTypes}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }} config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
inactiveFilters={inactiveLogsFilters}
actions={ actions={
<ButtonWrapperStyled> <ButtonWrapperStyled>
<Button type="primary" onClick={handleRunQuery}> <Button type="primary" onClick={handleRunQuery}>
@ -45,7 +56,6 @@ function LogsExplorer(): JSX.Element {
/> />
</Col> </Col>
<Col xs={24}> <Col xs={24}>
<LogsExplorerChart />
<LogsExplorerViews /> <LogsExplorerViews />
</Col> </Col>
</Row> </Row>

View File

@ -0,0 +1,12 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({
...query,
builder: {
...query.builder,
queryData: query.builder.queryData.map((item) => ({
...item,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
})),
},
});

View File

@ -2,10 +2,7 @@ import { Tabs } from 'antd';
import axios from 'axios'; import axios from 'axios';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
COMPOSITE_QUERY,
PANEL_TYPES_QUERY,
} from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import ExportPanel from 'container/ExportPanel'; import ExportPanel from 'container/ExportPanel';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
@ -102,11 +99,9 @@ function TracesExplorer(): JSX.Element {
onSuccess: (data) => { onSuccess: (data) => {
const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, { const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, {
dashboardId: data?.payload?.uuid, dashboardId: data?.payload?.uuid,
})}/new?${QueryParams.graphType}=graph&${ })}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${
QueryParams.widgetId queryParamNamesMap.compositeQuery
}=empty&${COMPOSITE_QUERY}=${encodeURIComponent( }=${encodeURIComponent(JSON.stringify(exportDefaultQuery))}`;
JSON.stringify(exportDefaultQuery),
)}`;
history.push(dashboardEditView); history.push(dashboardEditView);
}, },
@ -132,7 +127,9 @@ function TracesExplorer(): JSX.Element {
DataSource.TRACES, DataSource.TRACES,
); );
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType }); redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
}, },
[ [
currentQuery, currentQuery,

View File

@ -13,7 +13,7 @@ import {
MAX_QUERIES, MAX_QUERIES,
PANEL_TYPES, PANEL_TYPES,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
@ -461,7 +461,7 @@ export function QueryBuilderProvider({
}; };
urlQuery.set( urlQuery.set(
COMPOSITE_QUERY, queryParamNamesMap.compositeQuery,
encodeURIComponent(JSON.stringify(currentGeneratedQuery)), encodeURIComponent(JSON.stringify(currentGeneratedQuery)),
); );

View File

@ -1,7 +1,7 @@
import { notification } from 'antd'; import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update'; import updateDashboardApi from 'api/dashboard/update';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
@ -88,7 +88,7 @@ export const SaveDashboard = ({
}; };
const allLayout = getAllLayout(); const allLayout = getAllLayout();
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const compositeQuery = params.get(COMPOSITE_QUERY); const compositeQuery = params.get(queryParamNamesMap.compositeQuery);
const { maxTime, minTime } = store.getState().globalTime; const { maxTime, minTime } = store.getState().globalTime;
const query = compositeQuery const query = compositeQuery
? updateStepInterval( ? updateStepInterval(

View File

@ -1,6 +1,6 @@
export interface ILog { export interface ILog {
date: string; date: string;
timestamp: number; timestamp: number | string;
id: string; id: string;
traceId: string; traceId: string;
spanId: string; spanId: string;

View File

@ -59,6 +59,8 @@ export type IBuilderQuery = {
orderBy: OrderByPayload[]; orderBy: OrderByPayload[];
reduceTo: ReduceOperators; reduceTo: ReduceOperators;
legend: string; legend: string;
pageSize?: number;
offset?: number;
}; };
export interface IClickHouseQuery { export interface IClickHouseQuery {

View File

@ -5,7 +5,10 @@ export interface PayloadProps {
result: QueryData[]; result: QueryData[];
} }
export type ListItem = { timestamp: string; data: Omit<ILog, 'timestamp'> }; export type ListItem = {
timestamp: string;
data: Omit<ILog, 'timestamp'>;
};
export interface QueryData { export interface QueryData {
metric: { metric: {

View File

@ -1,11 +1,13 @@
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces'; import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from './select'; import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'>; type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'inactiveFilters'>;
export type HandleChangeQueryData = < export type HandleChangeQueryData = <
Key extends keyof IBuilderQuery, Key extends keyof IBuilderQuery,