feat: add logs context (#3190)

* feat: add the ability to share a link to a log line

* fix: update tooltip

* fix: resolve comments regarding query params

* fix: resolve comments

* feat: add logs context

* feat: add highlighting active items

* fix: resolve comments

* feat: fix showing log lines

* fix: update logs ordering

* fix: update page size and logs saving

* fix: update related to comments

* feat: logs context is updated

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
dnazarenkoo 2023-07-30 14:02:18 +03:00 committed by GitHub
parent bc4a4edc7f
commit 5f89e84eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1393 additions and 341 deletions

View File

@ -46,6 +46,7 @@
"chartjs-adapter-date-fns": "^2.0.0", "chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0", "chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1", "color": "^4.2.1",
"color-alpha": "1.1.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "4.3.0", "css-loader": "4.3.0",
"css-minimizer-webpack-plugin": "^3.2.0", "css-minimizer-webpack-plugin": "^3.2.0",

View File

@ -18,6 +18,7 @@ export const getMetricsQueryRange = async (
error: null, error: null,
message: response.data.status, message: response.data.status,
payload: response.data, payload: response.data,
params: props,
}; };
} catch (error) { } catch (error) {
return ErrorResponseHandler(error as AxiosError); return ErrorResponseHandler(error as AxiosError);

View File

@ -1,9 +1,10 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
export type LogDetailProps = { export type LogDetailProps = {
log: ILog | null; log: ILog | null;
onClose: () => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> & } & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'>; Pick<ActionItemProps, 'onClickActionItem'> &
Pick<DrawerProps, 'onClose'>;

View File

@ -11,10 +11,6 @@ function LogDetail({
onAddToQuery, onAddToQuery,
onClickActionItem, onClickActionItem,
}: LogDetailProps): JSX.Element { }: LogDetailProps): JSX.Element {
const onDrawerClose = (): void => {
onClose();
};
const items = useMemo( const items = useMemo(
() => [ () => [
{ {
@ -43,7 +39,7 @@ function LogDetail({
title="Log Details" title="Log Details"
placement="right" placement="right"
closable closable
onClose={onDrawerClose} onClose={onClose}
open={log !== null} open={log !== null}
style={{ overscrollBehavior: 'contain' }} style={{ overscrollBehavior: 'contain' }}
destroyOnClose destroyOnClose

View File

@ -1,9 +1,19 @@
import { blue, grey, orange } from '@ant-design/colors'; import { blue, grey, orange } from '@ant-design/colors';
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons'; import {
CopyFilled,
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import Convert from 'ansi-to-html'; import Convert from 'ansi-to-html';
import { Button, Divider, Row, Typography } from 'antd'; import { Button, Divider, Row, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
// utils // utils
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
@ -85,24 +95,40 @@ function LogSelectedField({
type ListLogViewProps = { type ListLogViewProps = {
logData: ILog; logData: ILog;
onOpenDetailedView: (log: ILog) => void;
selectedFields: IField[]; selectedFields: IField[];
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>; };
function ListLogView({ function ListLogView({
logData, logData,
selectedFields, selectedFields,
onOpenDetailedView,
onAddToQuery,
}: ListLogViewProps): JSX.Element { }: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const isDarkMode = useIsDarkMode();
const [, setCopy] = useCopyToClipboard(); const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
logData.id,
);
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const handleDetailedView = useCallback(() => { const handleDetailedView = useCallback(() => {
onOpenDetailedView(logData); onSetActiveLog(logData);
}, [logData, onOpenDetailedView]); }, [logData, onSetActiveLog]);
const handleShowContext = useCallback(() => {
handleSetActiveContextLog(logData);
}, [logData, handleSetActiveContextLog]);
const handleCopyJSON = (): void => { const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2)); setCopy(JSON.stringify(logData, null, 2));
@ -125,7 +151,7 @@ function ListLogView({
); );
return ( return (
<Container> <Container $isActiveLog={isHighlighted} $isDarkMode={isDarkMode}>
<div> <div>
<LogContainer> <LogContainer>
<> <>
@ -169,6 +195,42 @@ function ListLogView({
> >
Copy JSON Copy JSON
</Button> </Button>
{isLogsExplorerPage && (
<>
<Button
size="small"
type="text"
onClick={handleShowContext}
style={{ color: grey[1] }}
icon={<MonitorOutlined />}
>
Show in Context
</Button>
<Button
size="small"
type="text"
onClick={onLogCopy}
style={{ color: grey[1] }}
icon={<LinkOutlined />}
>
Copy Link
</Button>
</>
)}
{activeContextLog && (
<LogsExplorerContext
log={activeContextLog}
onClose={handleClearActiveContextLog}
/>
)}
<LogDetail
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</Row> </Row>
</Container> </Container>
); );

View File

@ -1,12 +1,26 @@
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components'; import styled from 'styled-components';
import getAlphaColor from 'utils/getAlphaColor';
export const Container = styled(Card)` export const Container = styled(Card)<{
$isDarkMode: boolean;
$isActiveLog: boolean;
}>`
width: 100% !important; width: 100% !important;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
.ant-card-body { .ant-card-body {
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
} }
${({ $isDarkMode, $isActiveLog }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode
? getAlphaColor(themeColors.white)[10]
: getAlphaColor(themeColors.black)[10]
};`
: ''}
`; `;
export const Text = styled(Typography.Text)` export const Text = styled(Typography.Text)`

View File

@ -1,16 +1,32 @@
import { ExpandAltOutlined } from '@ant-design/icons'; import {
// const Convert = require('ansi-to-html'); ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import Convert from 'ansi-to-html'; import Convert from 'ansi-to-html';
import { Button, DrawerProps, Tooltip } from 'antd';
import LogDetail from 'components/LogDetail';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks // hooks
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useCallback, useMemo } from 'react'; import {
KeyboardEvent,
MouseEvent,
MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
// interfaces // interfaces
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
// styles // styles
import { import {
ActionButtonsWrapper,
ExpandIconWrapper, ExpandIconWrapper,
RawLogContent, RawLogContent,
RawLogViewContainer, RawLogViewContainer,
@ -19,15 +35,34 @@ import {
const convert = new Convert(); const convert = new Convert();
interface RawLogViewProps { interface RawLogViewProps {
isActiveLog?: boolean;
isReadOnly?: boolean;
data: ILog; data: ILog;
linesPerRow: number; linesPerRow: number;
onClickExpand: (log: ILog) => void;
} }
function RawLogView(props: RawLogViewProps): JSX.Element { function RawLogView(props: RawLogViewProps): JSX.Element {
const { data, linesPerRow, onClickExpand } = props; const { isActiveLog = false, isReadOnly = false, data, linesPerRow } = props;
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
data.id,
);
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
const text = useMemo( const text = useMemo(
() => () =>
@ -38,8 +73,43 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
); );
const handleClickExpand = useCallback(() => { const handleClickExpand = useCallback(() => {
onClickExpand(data); if (activeContextLog || isReadOnly) return;
}, [onClickExpand, data]);
onSetActiveLog(data);
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
(
event: MouseEvent<Element, globalThis.MouseEvent> | KeyboardEvent<Element>,
) => {
event.preventDefault();
event.stopPropagation();
onClearActiveLog();
},
[onClearActiveLog],
);
const handleMouseEnter = useCallback(() => {
if (isReadOnlyLog) return;
setHasActionButtons(true);
}, [isReadOnlyLog]);
const handleMouseLeave = useCallback(() => {
if (isReadOnlyLog) return;
setHasActionButtons(false);
}, [isReadOnlyLog]);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
handleSetActiveContextLog(data);
},
[data, handleSetActiveContextLog],
);
const html = useMemo( const html = useMemo(
() => ({ () => ({
@ -48,19 +118,69 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
[text], [text],
); );
const mouseActions = useMemo(
() => ({ onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }),
[handleMouseEnter, handleMouseLeave],
);
return ( return (
<RawLogViewContainer <RawLogViewContainer
onClick={handleClickExpand} onClick={handleClickExpand}
wrap={false} wrap={false}
align="middle" align="middle"
$isDarkMode={isDarkMode} $isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isActiveLog={isHighlighted}
// eslint-disable-next-line react/jsx-props-no-spreading
{...mouseActions}
> >
<ExpandIconWrapper flex="30px"> {!isReadOnly && (
<ExpandAltOutlined /> <ExpandIconWrapper flex="30px">
</ExpandIconWrapper> <ExpandAltOutlined />
<RawLogContent linesPerRow={linesPerRow} dangerouslySetInnerHTML={html} /> </ExpandIconWrapper>
)}
<RawLogContent
$isReadOnly={isReadOnly}
$isActiveLog={isActiveLog}
linesPerRow={linesPerRow}
dangerouslySetInnerHTML={html}
/>
{hasActionButtons && (
<ActionButtonsWrapper>
<Tooltip title="Show Context">
<Button
size="small"
icon={<MonitorOutlined />}
onClick={handleShowContext}
/>
</Tooltip>
<Tooltip title="Copy Link">
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
</Tooltip>
</ActionButtonsWrapper>
)}
{activeContextLog && (
<LogsExplorerContext
log={activeContextLog}
onClose={handleClearActiveContextLog}
/>
)}
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</RawLogViewContainer> </RawLogViewContainer>
); );
} }
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,
};
export default RawLogView; export default RawLogView;

View File

@ -1,8 +1,15 @@
import { blue } from '@ant-design/colors'; import { blue, orange } from '@ant-design/colors';
import { Col, Row } from 'antd'; import { Col, Row, Space } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components'; import styled from 'styled-components';
import getAlphaColor from 'utils/getAlphaColor';
export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>` export const RawLogViewContainer = styled(Row)<{
$isDarkMode: boolean;
$isReadOnly: boolean;
$isActiveLog: boolean;
}>`
position: relative;
width: 100%; width: 100%;
font-weight: 700; font-weight: 700;
font-size: 0.625rem; font-size: 0.625rem;
@ -10,10 +17,19 @@ export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>`
transition: background-color 0.2s ease-in; transition: background-color 0.2s ease-in;
&:hover { ${({ $isActiveLog }): string =>
background-color: ${({ $isDarkMode }): string => $isActiveLog ? `background-color: ${orange[3]};` : ''}
$isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0, 0, 0, 0.1)'};
} ${({ $isReadOnly, $isDarkMode }): string =>
!$isReadOnly
? `&:hover {
background-color: ${
$isDarkMode
? getAlphaColor(themeColors.white)[10]
: getAlphaColor(themeColors.black)[10]
};
}`
: ''}
`; `;
export const ExpandIconWrapper = styled(Col)` export const ExpandIconWrapper = styled(Col)`
@ -25,6 +41,8 @@ export const ExpandIconWrapper = styled(Col)`
interface RawLogContentProps { interface RawLogContentProps {
linesPerRow: number; linesPerRow: number;
$isReadOnly: boolean;
$isActiveLog: boolean;
} }
export const RawLogContent = styled.div<RawLogContentProps>` export const RawLogContent = styled.div<RawLogContentProps>`
@ -42,5 +60,17 @@ export const RawLogContent = styled.div<RawLogContentProps>`
font-size: 1rem; font-size: 1rem;
line-height: 2rem; line-height: 2rem;
cursor: ${(props): string =>
props.$isActiveLog || props.$isReadOnly ? 'initial' : 'pointer'};
${(props): string =>
props.$isReadOnly && !props.$isActiveLog ? 'padding: 0 1.5rem;' : ''}
`;
export const ActionButtonsWrapper = styled(Space)`
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
right: 0;
cursor: pointer; cursor: pointer;
`; `;

View File

@ -10,7 +10,6 @@ export type LogsTableViewProps = {
logs: ILog[]; logs: ILog[];
fields: IField[]; fields: IField[];
linesPerRow: number; linesPerRow: number;
onClickExpand: (log: ILog) => void;
}; };
export type UseTableViewResult = { export type UseTableViewResult = {
@ -20,4 +19,12 @@ export type UseTableViewResult = {
export type UseTableViewProps = { export type UseTableViewProps = {
appendTo?: 'center' | 'end'; appendTo?: 'center' | 'end';
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
} & LogsTableViewProps; } & LogsTableViewProps;
export type ActionsColumnProps = {
logId: string;
logs: ILog[];
onOpenLogsContext?: (log: ILog) => void;
};

View File

@ -1,16 +1,22 @@
import { ExpandAltOutlined } from '@ant-design/icons'; import {
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import Convert from 'ansi-to-html'; import Convert from 'ansi-to-html';
import { Typography } from 'antd'; import { Button, Space, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ExpandIconWrapper } from '../RawLogView/styles'; import { ExpandIconWrapper } from '../RawLogView/styles';
import { defaultCellStyle, defaultTableStyle } from './config'; import { defaultCellStyle, defaultTableStyle } from './config';
import { TableBodyContent } from './styles'; import { TableBodyContent } from './styles';
import { import {
ActionsColumnProps,
ColumnTypeRender, ColumnTypeRender,
UseTableViewProps, UseTableViewProps,
UseTableViewResult, UseTableViewResult,
@ -18,19 +24,60 @@ import {
const convert = new Convert(); const convert = new Convert();
function ActionsColumn({
logId,
logs,
onOpenLogsContext,
}: ActionsColumnProps): JSX.Element {
const currentLog = useMemo(() => logs.find(({ id }) => id === logId), [
logs,
logId,
]);
const { onLogCopy } = useCopyLogLink(currentLog?.id);
const handleShowContext = useCallback(() => {
if (!onOpenLogsContext || !currentLog) return;
onOpenLogsContext(currentLog);
}, [currentLog, onOpenLogsContext]);
return (
<Space>
<Button
size="small"
onClick={handleShowContext}
icon={<MonitorOutlined />}
/>
<Button size="small" onClick={onLogCopy} icon={<LinkOutlined />} />
</Space>
);
}
export const useTableView = (props: UseTableViewProps): UseTableViewResult => { export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const { const {
logs, logs,
fields, fields,
linesPerRow, linesPerRow,
onClickExpand,
appendTo = 'center', appendTo = 'center',
onOpenLogsContext,
onClickExpand,
} = props; } = props;
const { isLogsExplorerPage } = useCopyLogLink();
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [ const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs, logs,
]); ]);
const handleClickExpand = useCallback(
(index: number): void => {
if (!onClickExpand) return;
onClickExpand(logs[index]);
},
[logs, onClickExpand],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => { const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id') .filter((e) => e.name !== 'id')
@ -63,7 +110,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
children: ( children: (
<ExpandIconWrapper <ExpandIconWrapper
onClick={(): void => { onClick={(): void => {
onClickExpand(logs[index]); handleClickExpand(index);
}} }}
> >
<ExpandAltOutlined /> <ExpandAltOutlined />
@ -106,8 +153,34 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}), }),
}, },
...(appendTo === 'end' ? fieldColumns : []), ...(appendTo === 'end' ? fieldColumns : []),
...(isLogsExplorerPage
? ([
{
title: 'actions',
dataIndex: 'actions',
key: 'actions',
render: (_, log): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<ActionsColumn
logId={(log.id as unknown) as string}
logs={logs}
onOpenLogsContext={onOpenLogsContext}
/>
),
}),
},
] as ColumnsType<Record<string, unknown>>)
: []),
]; ];
}, [fields, appendTo, linesPerRow, onClickExpand, logs]); }, [
logs,
fields,
appendTo,
linesPerRow,
isLogsExplorerPage,
handleClickExpand,
onOpenLogsContext,
]);
return { columns, dataSource: flattenLogData }; return { columns, dataSource: flattenLogData };
}; };

View File

@ -16,4 +16,6 @@ export enum QueryParams {
widgetId = 'widgetId', widgetId = 'widgetId',
order = 'order', order = 'order',
q = 'q', q = 'q',
activeLogId = 'activeLogId',
timeRange = 'timeRange',
} }

View File

@ -38,6 +38,7 @@ const themeColors = {
whiteCream: '#ffffffd5', whiteCream: '#ffffffd5',
white: '#ffffff', white: '#ffffff',
black: '#000000', black: '#000000',
darkGrey: '#262626',
lightBlack: '#141414', lightBlack: '#141414',
lightgrey: '#ddd', lightgrey: '#ddd',
lightWhite: '#ffffffd9', lightWhite: '#ffffffd9',

View File

@ -0,0 +1,36 @@
import { Button, Typography } from 'antd';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ShowButtonWrapper } from './styles';
interface ShowButtonProps {
isLoading: boolean;
isDisabled: boolean;
order: string;
onClick: () => void;
}
function ShowButton({
isLoading,
isDisabled,
order,
onClick,
}: ShowButtonProps): JSX.Element {
return (
<ShowButtonWrapper>
<Typography>
Showing 10 lines {order === FILTERS.ASC ? 'after' : 'before'} match
</Typography>
<Button
size="small"
disabled={isLoading || isDisabled}
loading={isLoading}
onClick={onClick}
>
Show 10 more lines
</Button>
</ShowButtonWrapper>
);
}
export default ShowButton;

View File

@ -0,0 +1,9 @@
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
export const INITIAL_PAGE_SIZE = 5;
export const LOGS_MORE_PAGE_SIZE = 10;
export const getOrderByTimestamp = (order: string): OrderByPayload => ({
columnName: 'timestamp',
order,
});

View File

@ -0,0 +1,198 @@
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { SuccessResponse } from 'types/api';
import { ILog } from 'types/api/logs/log';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
getOrderByTimestamp,
INITIAL_PAGE_SIZE,
LOGS_MORE_PAGE_SIZE,
} from './configs';
import ShowButton from './ShowButton';
import { EmptyText, ListContainer } from './styles';
import { getRequestData } from './utils';
interface LogsContextListProps {
isEdit: boolean;
query: Query;
log: ILog;
order: string;
filters: TagFilter | null;
}
function LogsContextList({
isEdit,
query,
log,
order,
filters,
}: LogsContextListProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [logs, setLogs] = useState<ILog[]>([]);
const [page, setPage] = useState<number>(1);
const firstLog = useMemo(() => logs[0], [logs]);
const lastLog = useMemo(() => logs[logs.length - 1], [logs]);
const orderByTimestamp = useMemo(() => getOrderByTimestamp(order), [order]);
const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [
page,
]);
const pageSize = useMemo(
() => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE),
[page, logsMorePageSize],
);
const isDisabledFetch = useMemo(() => logs.length < pageSize, [
logs.length,
pageSize,
]);
const currentStagedQueryData = useMemo(() => {
if (!query || query.builder.queryData.length !== 1) return null;
return query.builder.queryData[0];
}, [query]);
const initialLogsRequest = useMemo(
() =>
getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log,
orderByTimestamp,
page,
}),
[currentStagedQueryData, page, log, query, orderByTimestamp],
);
const [requestData, setRequestData] = useState<Query | null>(
initialLogsRequest,
);
const handleSuccess = useCallback(
(data: SuccessResponse<MetricRangePayloadProps, unknown>) => {
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,
}));
if (order === FILTERS.ASC) {
const reversedCurrentLogs = currentLogs.reverse();
setLogs((prevLogs) => [...reversedCurrentLogs, ...prevLogs]);
} else {
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
}
}
},
[order],
);
const { isError, isFetching } = useGetExplorerQueryRange(
requestData,
PANEL_TYPES.LIST,
{
keepPreviousData: true,
onSuccess: handleSuccess,
},
);
const handleShowNextLines = useCallback(() => {
if (isDisabledFetch) return;
const log = order === FILTERS.ASC ? firstLog : lastLog;
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log,
orderByTimestamp,
page: page + 1,
pageSize: LOGS_MORE_PAGE_SIZE,
});
setPage((prevPage) => prevPage + 1);
setRequestData(newRequestData);
}, [
query,
firstLog,
lastLog,
page,
order,
currentStagedQueryData,
isDisabledFetch,
orderByTimestamp,
]);
useEffect(() => {
if (!isEdit) return;
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
query,
log,
orderByTimestamp,
page: 1,
});
setPage(1);
setLogs([]);
setRequestData(newRequestData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => (
<RawLogView isReadOnly key={log.id} data={log} linesPerRow={1} />
),
[],
);
return (
<>
{order === FILTERS.ASC && (
<ShowButton
isLoading={isFetching}
isDisabled={isDisabledFetch}
order={order}
onClick={handleShowNextLines}
/>
)}
<ListContainer $isDarkMode={isDarkMode}>
{((!logs.length && !isFetching) || isError) && (
<EmptyText>No Data</EmptyText>
)}
{isFetching && <Spinner size="large" height="10rem" />}
<Virtuoso
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === FILTERS.DESC}
/>
</ListContainer>
{order === FILTERS.DESC && (
<ShowButton
isLoading={isFetching}
isDisabled={isDisabledFetch}
order={order}
onClick={handleShowNextLines}
/>
)}
</>
);
}
export default memo(LogsContextList);

View File

@ -0,0 +1,25 @@
import { Space, Typography } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const ListContainer = styled.div<{ $isDarkMode: boolean }>`
position: relative;
margin: 0 -1.5rem;
height: 10rem;
overflow-y: scroll;
background-color: ${({ $isDarkMode }): string =>
$isDarkMode ? themeColors.darkGrey : themeColors.lightgrey};
`;
export const ShowButtonWrapper = styled(Space)`
margin: 0.625rem 0;
`;
export const EmptyText = styled(Typography)`
padding: 0 1.5rem;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;

View File

@ -0,0 +1,52 @@
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { ILog } from 'types/api/logs/log';
import {
IBuilderQuery,
OrderByPayload,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { INITIAL_PAGE_SIZE } from './configs';
type GetRequestDataProps = {
query: Query | null;
stagedQueryData: IBuilderQuery | null;
log: ILog;
orderByTimestamp: OrderByPayload;
page: number;
pageSize?: number;
};
export const getRequestData = ({
query,
stagedQueryData,
log,
orderByTimestamp,
page,
pageSize = INITIAL_PAGE_SIZE,
}: GetRequestDataProps): Query | null => {
if (!query) return null;
const paginateData = getPaginationQueryData({
currentStagedQueryData: stagedQueryData,
listItemId: log ? log.id : null,
orderByTimestamp,
page,
pageSize,
});
const data: Query = {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData.map((item) => ({
...item,
...paginateData,
pageSize,
orderBy: [orderByTimestamp],
})),
},
};
return data;
};

View File

@ -0,0 +1,109 @@
import { EditFilled } from '@ant-design/icons';
import { Typography } from 'antd';
import Modal from 'antd/es/modal/Modal';
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo, useCallback, useMemo, useState } from 'react';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EditButton, TitleWrapper } from './styles';
import { LogsExplorerContextProps } from './types';
import useInitialQuery from './useInitialQuery';
function LogsExplorerContext({
log,
onClose,
}: LogsExplorerContextProps): JSX.Element | null {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query>(initialContextQuery);
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const handleClickEditButton = useCallback(
() => setIsEdit((prevValue) => !prevValue),
[],
);
const handleSearch = useCallback(
(tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length
)
return;
const nextQuery: Query = {
...contextQuery,
builder: {
...contextQuery.builder,
queryData: contextQuery.builder.queryData.map((item) => ({
...item,
filters: tagFilters,
})),
},
};
setFilters(tagFilters);
setContextQuery(nextQuery);
},
[contextQuery, filters],
);
const contextListParams = useMemo(
() => ({ log, isEdit, filters, query: contextQuery }),
[isEdit, log, filters, contextQuery],
);
return (
<Modal
centered
destroyOnClose
open
width={816}
onCancel={onClose}
onOk={onClose}
footer={null}
title={
<TitleWrapper block>
<Typography>Logs Context</Typography>
<EditButton
$isDarkMode={isDarkMode}
size="small"
type="text"
icon={<EditFilled />}
onClick={handleClickEditButton}
/>
</TitleWrapper>
}
>
{isEdit && (
<QueryBuilderSearch
query={contextQuery?.builder.queryData[0]}
onChange={handleSearch}
/>
)}
<LogsContextList
order={FILTERS.ASC}
// eslint-disable-next-line react/jsx-props-no-spreading
{...contextListParams}
/>
<RawLogView isActiveLog isReadOnly data={log} linesPerRow={1} />
<LogsContextList
order={FILTERS.DESC}
// eslint-disable-next-line react/jsx-props-no-spreading
{...contextListParams}
/>
</Modal>
);
}
export default memo(LogsExplorerContext);

View File

@ -0,0 +1,30 @@
import { Button, Space } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
import getAlphaColor from 'utils/getAlphaColor';
export const TitleWrapper = styled(Space.Compact)`
justify-content: space-between;
align-items: center;
`;
export const EditButton = styled(Button)<{ $isDarkMode: boolean }>`
margin-right: 0.938rem;
width: 1.375rem !important;
height: 1.375rem;
position: absolute;
top: 1rem;
right: 1.563rem;
padding: 0;
border-radius: 0.125rem;
border-start-start-radius: 0.125rem !important;
border-end-start-radius: 0.125rem !important;
color: ${({ $isDarkMode }): string =>
$isDarkMode
? getAlphaColor(themeColors.white)[45]
: getAlphaColor(themeColors.black)[45]};
`;

View File

@ -0,0 +1,6 @@
import { ILog } from 'types/api/logs/log';
export interface LogsExplorerContextProps {
log: ILog;
onClose: VoidFunction;
}

View File

@ -0,0 +1,36 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ILog } from 'types/api/logs/log';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getFiltersFromResources } from './utils';
const useInitialQuery = (log: ILog): Query => {
const { updateAllQueriesOperators } = useQueryBuilder();
const resourcesFilters = getFiltersFromResources(log.resources_string);
const updatedAllQueriesOperator = updateAllQueriesOperators(
initialQueriesMap.logs,
PANEL_TYPES.LIST,
DataSource.LOGS,
);
const data: Query = {
...updatedAllQueriesOperator,
builder: {
...updatedAllQueriesOperator.builder,
queryData: updatedAllQueriesOperator.builder.queryData.map((item) => ({
...item,
filters: {
...item.filters,
items: [...item.filters.items, ...resourcesFilters],
},
})),
},
};
return data;
};
export default useInitialQuery;

View File

@ -0,0 +1,22 @@
import { OPERATORS } from 'constants/queryBuilder';
import { ILog } from 'types/api/logs/log';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const getFiltersFromResources = (
resources: ILog['resources_string'],
): TagFilterItem[] =>
Object.keys(resources).map((key: string) => {
const resourceValue = resources[key] as string;
return {
id: uuid(),
key: {
key,
dataType: 'string',
type: 'resource',
isColumn: false,
},
op: OPERATORS['='],
value: resourceValue,
};
});

View File

@ -1,3 +1,4 @@
import Spinner from 'components/Spinner';
import { dragColumnParams } from 'hooks/useDragColumns/configs'; import { dragColumnParams } from 'hooks/useDragColumns/configs';
import ReactDragListView from 'react-drag-listview'; import ReactDragListView from 'react-drag-listview';
import { TableComponents } from 'react-virtuoso'; import { TableComponents } from 'react-virtuoso';
@ -5,13 +6,18 @@ import { TableComponents } from 'react-virtuoso';
import { TableStyled } from './styles'; import { TableStyled } from './styles';
interface LogsCustomTableProps { interface LogsCustomTableProps {
isLoading?: boolean;
handleDragEnd: (fromIndex: number, toIndex: number) => void; handleDragEnd: (fromIndex: number, toIndex: number) => void;
} }
export const LogsCustomTable = ({ export const LogsCustomTable = ({
isLoading,
handleDragEnd, handleDragEnd,
}: LogsCustomTableProps): TableComponents['Table'] => }: LogsCustomTableProps): TableComponents['Table'] =>
function CustomTable({ style, children }): JSX.Element { function CustomTable({ style, children }): JSX.Element {
if (isLoading) {
return <Spinner height="35px" tip="Getting Logs" />;
}
return ( return (
<ReactDragListView.DragColumn <ReactDragListView.DragColumn
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading

View File

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

View File

@ -1,16 +1,27 @@
import LogDetail from 'components/LogDetail';
import { ColumnTypeRender } from 'components/Logs/TableView/types'; import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { useTableView } from 'components/Logs/TableView/useTableView'; import { useTableView } from 'components/Logs/TableView/useTableView';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import LogsExplorerContext from 'container/LogsExplorerContext';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDragColumns from 'hooks/useDragColumns'; import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils'; import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import { import {
cloneElement, cloneElement,
forwardRef,
ReactElement, ReactElement,
ReactNode, ReactNode,
useCallback, useCallback,
useMemo, useMemo,
} from 'react'; } from 'react';
import { TableComponents, TableVirtuoso } from 'react-virtuoso'; import {
TableComponents,
TableVirtuoso,
TableVirtuosoHandle,
} from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { infinityDefaultStyles } from './config'; import { infinityDefaultStyles } from './config';
import { LogsCustomTable } from './LogsCustomTable'; import { LogsCustomTable } from './LogsCustomTable';
@ -22,105 +33,153 @@ import {
import { InfinityTableProps } from './types'; import { InfinityTableProps } from './types';
// eslint-disable-next-line react/function-component-definition // eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents['TableRow'] = ({ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
children, children,
context, context,
...props ...props
// eslint-disable-next-line react/jsx-props-no-spreading }) => {
}) => <TableRowStyled {...props}>{children}</TableRowStyled>; const isDarkMode = useIsDarkMode();
const { isHighlighted } = useCopyLogLink(props.item.id);
function InfinityTable({
tableViewProps,
infitiyTableProps,
}: InfinityTableProps): JSX.Element | null {
const { onEndReached } = infitiyTableProps;
const { dataSource, columns } = useTableView(tableViewProps);
const { draggedColumns, onDragColumns } = useDragColumns<
Record<string, unknown>
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const tableColumns = useMemo(
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
[columns, draggedColumns],
);
const handleDragEnd = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(tableColumns, fromIndex, toIndex),
[tableColumns, onDragColumns],
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<>
{tableColumns.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>
);
})}
</>
),
[tableColumns],
);
const tableHeader = useCallback(
() => (
<tr>
{tableColumns.map((column) => {
const isDragColumn = column.key !== 'expand';
return (
<TableHeaderCellStyled
isDragColumn={isDragColumn}
key={column.key}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
>
{column.title as string}
</TableHeaderCellStyled>
);
})}
</tr>
),
[tableColumns],
);
return ( return (
<TableVirtuoso <TableRowStyled
style={infinityDefaultStyles} $isDarkMode={isDarkMode}
data={dataSource} $isActiveLog={isHighlighted}
components={{ // eslint-disable-next-line react/jsx-props-no-spreading
// eslint-disable-next-line react/jsx-props-no-spreading {...props}
Table: LogsCustomTable({ handleDragEnd }), >
// TODO: fix it in the future {children}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment </TableRowStyled>
// @ts-ignore
TableRow: CustomTableRow,
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}
endReached={onEndReached}
totalCount={dataSource.length}
/>
); );
} };
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{ isLoading, tableViewProps, infitiyTableProps },
ref,
): JSX.Element | null {
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const { onEndReached } = infitiyTableProps;
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
});
const { draggedColumns, onDragColumns } = useDragColumns<
Record<string, unknown>
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const tableColumns = useMemo(
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
[columns, draggedColumns],
);
const handleDragEnd = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(tableColumns, fromIndex, toIndex),
[tableColumns, onDragColumns],
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<>
{tableColumns.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>
);
})}
</>
),
[tableColumns],
);
const tableHeader = useCallback(
() => (
<tr>
{tableColumns.map((column) => {
const isDragColumn = column.key !== 'expand';
return (
<TableHeaderCellStyled
isDragColumn={isDragColumn}
key={column.key}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
>
{column.title as string}
</TableHeaderCellStyled>
);
})}
</tr>
),
[tableColumns],
);
return (
<>
<TableVirtuoso
useWindowScroll
ref={ref}
style={infinityDefaultStyles}
data={dataSource}
components={{
// eslint-disable-next-line react/jsx-props-no-spreading
Table: LogsCustomTable({ isLoading, handleDragEnd }),
// 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}
/>
{activeContextLog && (
<LogsExplorerContext
log={activeContextLog}
onClose={handleClearActiveContextLog}
/>
)}
<LogDetail
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</>
);
},
);
export default InfinityTable; export default InfinityTable;

View File

@ -22,7 +22,19 @@ export const TableCellStyled = styled.td`
background-color: ${themeColors.lightBlack}; background-color: ${themeColors.lightBlack};
`; `;
export const TableRowStyled = styled.tr` export const TableRowStyled = styled.tr<{
$isDarkMode: boolean;
$isActiveLog: boolean;
}>`
td {
${({ $isDarkMode, $isActiveLog }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0, 0, 0, 0.1)'
};`
: ''}
}
&:hover { &:hover {
${TableCellStyled} { ${TableCellStyled} {
background-color: #1d1d1d; background-color: #1d1d1d;

View File

@ -1,7 +1,8 @@
import { UseTableViewProps } from 'components/Logs/TableView/types'; import { UseTableViewProps } from 'components/Logs/TableView/types';
export type InfinityTableProps = { export type InfinityTableProps = {
tableViewProps: UseTableViewProps; isLoading?: boolean;
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
infitiyTableProps: { infitiyTableProps: {
onEndReached: (index: number) => void; onEndReached: (index: number) => void;
}; };

View File

@ -1,4 +1,3 @@
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@ -7,6 +6,4 @@ export type LogsExplorerListProps = {
currentStagedQueryData: IBuilderQuery | null; currentStagedQueryData: IBuilderQuery | null;
logs: ILog[]; logs: ILog[];
onEndReached: (index: number) => void; onEndReached: (index: number) => void;
onExpand: (log: ILog) => void; };
onOpenDetailedView: (log: ILog) => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;

View File

@ -8,10 +8,11 @@ import ExplorerControlPanel from 'container/ExplorerControlPanel';
import { Heading } from 'container/LogsTable/styles'; import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import { contentStyle } from 'container/Trace/Search/config'; import { contentStyle } from 'container/Trace/Search/config';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useFontFaceObserver from 'hooks/useFontObserver'; import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso, VirtuosoHandle } 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 { DataSource, StringOperators } from 'types/common/queryBuilder';
@ -29,13 +30,13 @@ function LogsExplorerList({
isLoading, isLoading,
currentStagedQueryData, currentStagedQueryData,
logs, logs,
onOpenDetailedView,
onEndReached, onEndReached,
onExpand,
onAddToQuery,
}: LogsExplorerListProps): JSX.Element { }: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { initialDataSource } = useQueryBuilder(); const { initialDataSource } = useQueryBuilder();
const { activeLogId } = useCopyLogLink();
const { options, config } = useOptionsMenu({ const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS, storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS, dataSource: initialDataSource || DataSource.METRICS,
@ -43,6 +44,11 @@ function LogsExplorerList({
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP, currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
}); });
const activeLogIndex = useMemo(
() => logs.findIndex(({ id }) => id === activeLogId),
[logs, activeLogId],
);
useFontFaceObserver( useFontFaceObserver(
[ [
{ {
@ -65,35 +71,27 @@ function LogsExplorerList({
(_: number, log: ILog): JSX.Element => { (_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') { if (options.format === 'raw') {
return ( return (
<RawLogView <RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
key={log.id}
data={log}
linesPerRow={options.maxLines}
onClickExpand={onExpand}
/>
); );
} }
return ( return (
<ListLogView <ListLogView key={log.id} logData={log} selectedFields={selectedFields} />
key={log.id}
logData={log}
selectedFields={selectedFields}
onOpenDetailedView={onOpenDetailedView}
onAddToQuery={onAddToQuery}
/>
); );
}, },
[ [options.format, options.maxLines, selectedFields],
options.format,
options.maxLines,
selectedFields,
onOpenDetailedView,
onAddToQuery,
onExpand,
],
); );
useEffect(() => {
if (!activeLogId || activeLogIndex < 0) return;
ref?.current?.scrollToIndex({
index: activeLogIndex,
align: 'start',
behavior: 'smooth',
});
}, [activeLogId, activeLogIndex]);
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
const components = isLoading const components = isLoading
? { ? {
@ -104,11 +102,12 @@ function LogsExplorerList({
if (options.format === 'table') { if (options.format === 'table') {
return ( return (
<InfinityTableView <InfinityTableView
ref={ref}
isLoading={isLoading}
tableViewProps={{ tableViewProps={{
logs, logs,
fields: selectedFields, fields: selectedFields,
linesPerRow: options.maxLines, linesPerRow: options.maxLines,
onClickExpand: onExpand,
appendTo: 'end', appendTo: 'end',
}} }}
infitiyTableProps={{ onEndReached }} infitiyTableProps={{ onEndReached }}
@ -119,6 +118,7 @@ function LogsExplorerList({
return ( return (
<Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}> <Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
<Virtuoso <Virtuoso
ref={ref}
useWindowScroll useWindowScroll
data={logs} data={logs}
endReached={onEndReached} endReached={onEndReached}
@ -136,7 +136,6 @@ function LogsExplorerList({
onEndReached, onEndReached,
getItemContent, getItemContent,
selectedFields, selectedFields,
onExpand,
]); ]);
return ( return (

View File

@ -1,13 +1,10 @@
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import LogDetail from 'components/LogDetail';
import TabLabel from 'components/TabLabel'; import TabLabel from 'components/TabLabel';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { import {
initialAutocompleteData, initialAutocompleteData,
initialQueriesMap, initialQueriesMap,
OPERATORS,
PANEL_TYPES, PANEL_TYPES,
QueryBuilderKeys,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -21,25 +18,20 @@ import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/const
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils'; import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { LogTimeRange } from 'hooks/logs/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError'; import useAxiosError from 'hooks/useAxiosError';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { generatePath, useHistory } from 'react-router-dom'; import { generatePath, useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { import {
IBuilderQuery, IBuilderQuery,
OrderByPayload, OrderByPayload,
@ -47,7 +39,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData'; } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled'; import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled';
@ -55,8 +46,7 @@ function LogsExplorerViews(): JSX.Element {
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const history = useHistory(); const history = useHistory();
const queryClient = useQueryClient(); const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
const { queryData: pageSize } = useUrlQueryData( const { queryData: pageSize } = useUrlQueryData(
queryParamNamesMap.pageSize, queryParamNamesMap.pageSize,
DEFAULT_PER_PAGE_VALUE, DEFAULT_PER_PAGE_VALUE,
@ -79,7 +69,6 @@ function LogsExplorerViews(): JSX.Element {
} = useQueryBuilder(); } = useQueryBuilder();
// State // State
const [activeLog, setActiveLog] = useState<ILog | null>(null);
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]); const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null); const [requestData, setRequestData] = useState<Query | null>(null);
@ -167,16 +156,16 @@ function LogsExplorerViews(): JSX.Element {
keepPreviousData: true, keepPreviousData: true,
enabled: !isLimit, enabled: !isLimit,
}, },
{
...(timeRange &&
activeLogId &&
!logs.length && {
start: timeRange.start,
end: timeRange.end,
}),
},
); );
const handleSetActiveLog = useCallback((nextActiveLog: ILog) => {
setActiveLog(nextActiveLog);
}, []);
const handleClearActiveLog = useCallback(() => {
setActiveLog(null);
}, []);
const getUpdateQuery = useCallback( const getUpdateQuery = useCallback(
(newPanelType: PANEL_TYPES): Query => { (newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators( let query = updateAllQueriesOperators(
@ -245,51 +234,6 @@ function LogsExplorerViews(): JSX.Element {
[currentStagedQueryData, orderByTimestamp], [currentStagedQueryData, orderByTimestamp],
); );
const handleAddToQuery = useCallback(
(fieldKey: string, fieldValue: string, operator: string): void => {
const keysAutocomplete: BaseAutocompleteData[] =
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>(
[QueryBuilderKeys.GET_AGGREGATE_KEYS],
{ exact: false },
)?.payload.attributeKeys || [];
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
keysAutocomplete,
fieldKey,
);
const currentOperator =
Object.keys(OPERATORS).find((op) => op === operator) || '';
const nextQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filters: {
...item.filters,
items: [
...item.filters.items.filter(
(item) => item.key?.id !== existAutocompleteKey.id,
),
{
id: uuid(),
key: existAutocompleteKey,
op: currentOperator,
value: fieldValue,
},
],
},
})),
},
};
redirectWithQueryBuilderData(nextQuery);
},
[currentQuery, queryClient, redirectWithQueryBuilderData],
);
const handleEndReached = useCallback( const handleEndReached = useCallback(
(index: number) => { (index: number) => {
if (isLimit) return; if (isLimit) return;
@ -397,14 +341,24 @@ function LogsExplorerViews(): JSX.Element {
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]); }, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
useEffect(() => { useEffect(() => {
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
const currentData = data?.payload.data.newResult.data.result || []; const currentData = data?.payload.data.newResult.data.result || [];
if (currentData.length > 0 && currentData[0].list) { if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({ const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data, ...item.data,
timestamp: item.timestamp, timestamp: item.timestamp,
})); }));
setLogs((prevLogs) => [...prevLogs, ...currentLogs]); const newLogs = [...logs, ...currentLogs];
setLogs(newLogs);
onTimeRangeChange({
start: currentParams?.start,
end: timeRange?.end || currentParams?.end,
pageSize: newLogs.length,
});
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]); }, [data]);
useEffect(() => { useEffect(() => {
@ -415,14 +369,28 @@ function LogsExplorerViews(): JSX.Element {
const newRequestData = getRequestData(stagedQuery, { const newRequestData = getRequestData(stagedQuery, {
page: 1, page: 1,
log: null, log: null,
pageSize, pageSize:
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
}); });
setLogs([]); setLogs([]);
setPage(1); setPage(1);
setRequestData(newRequestData); setRequestData(newRequestData);
currentMinTimeRef.current = minTime; currentMinTimeRef.current = minTime;
if (!activeLogId) {
onTimeRangeChange(null);
}
} }
}, [stagedQuery, requestData, getRequestData, pageSize, minTime]); }, [
stagedQuery,
requestData,
getRequestData,
pageSize,
minTime,
timeRange,
activeLogId,
onTimeRangeChange,
]);
const tabsItems: TabsProps['items'] = useMemo( const tabsItems: TabsProps['items'] = useMemo(
() => [ () => [
@ -441,10 +409,7 @@ function LogsExplorerViews(): JSX.Element {
isLoading={isFetching} isLoading={isFetching}
currentStagedQueryData={currentStagedQueryData} currentStagedQueryData={currentStagedQueryData}
logs={logs} logs={logs}
onOpenDetailedView={handleSetActiveLog}
onEndReached={handleEndReached} onEndReached={handleEndReached}
onExpand={handleSetActiveLog}
onAddToQuery={handleAddToQuery}
/> />
), ),
}, },
@ -472,9 +437,7 @@ function LogsExplorerViews(): JSX.Element {
isFetching, isFetching,
currentStagedQueryData, currentStagedQueryData,
logs, logs,
handleSetActiveLog,
handleEndReached, handleEndReached,
handleAddToQuery,
data, data,
isError, isError,
], ],
@ -524,12 +487,6 @@ function LogsExplorerViews(): JSX.Element {
onChange={handleChangeView} onChange={handleChangeView}
destroyInactiveTabPane destroyInactiveTabPane
/> />
<LogDetail
log={activeLog}
onClose={handleClearActiveLog}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
/>
<GoToTop /> <GoToTop />
</> </>

View File

@ -4,18 +4,13 @@ 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 LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import ROUTES from 'constants/routes';
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 { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
// interfaces // interfaces
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
// styles // styles
@ -26,15 +21,10 @@ export type LogViewMode = 'raw' | 'table' | 'list';
type LogsTableProps = { type LogsTableProps = {
viewMode: LogViewMode; viewMode: LogViewMode;
linesPerRow: number; linesPerRow: number;
onClickExpand: (logData: ILog) => void;
}; };
function LogsTable(props: LogsTableProps): JSX.Element { function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, onClickExpand, linesPerRow } = props; const { viewMode, linesPerRow } = props;
const history = useHistory();
const dispatch = useDispatch();
useFontFaceObserver( useFontFaceObserver(
[ [
@ -52,7 +42,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
const { const {
logs, logs,
fields: { selected }, fields: { selected },
searchFilter: { queryString },
isLoading, isLoading,
liveTail, liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs); } = useSelector<AppState, ILogsReducer>((state) => state.logs);
@ -67,75 +56,23 @@ function LogsTable(props: LogsTableProps): JSX.Element {
liveTail, liveTail,
]); ]);
const handleOpenDetailedView = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const handleAddToQuery = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => {
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
operator,
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
},
[history, queryString],
);
const getItemContent = useCallback( const getItemContent = useCallback(
(index: number): JSX.Element => { (index: number): JSX.Element => {
const log = logs[index]; const log = logs[index];
if (viewMode === 'raw') { if (viewMode === 'raw') {
return ( return <RawLogView key={log.id} data={log} linesPerRow={linesPerRow} />;
<RawLogView
key={log.id}
data={log}
linesPerRow={linesPerRow}
onClickExpand={onClickExpand}
/>
);
} }
return ( return <ListLogView key={log.id} logData={log} selectedFields={selected} />;
<ListLogView
key={log.id}
logData={log}
selectedFields={selected}
onOpenDetailedView={handleOpenDetailedView}
onAddToQuery={handleAddToQuery}
/>
);
}, },
[ [logs, viewMode, selected, linesPerRow],
logs,
viewMode,
selected,
linesPerRow,
onClickExpand,
handleOpenDetailedView,
handleAddToQuery,
],
); );
const renderContent = useMemo(() => { const renderContent = useMemo(() => {
if (viewMode === 'table') { if (viewMode === 'table') {
return ( return (
<LogsTableView <LogsTableView logs={logs} fields={selected} linesPerRow={linesPerRow} />
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
onClickExpand={onClickExpand}
/>
); );
} }
@ -148,7 +85,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
/> />
</Card> </Card>
); );
}, [getItemContent, linesPerRow, logs, onClickExpand, selected, viewMode]); }, [getItemContent, linesPerRow, logs, selected, viewMode]);
if (isLoading) { if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />; return <Spinner height={20} tip="Getting Logs" />;

View File

@ -0,0 +1 @@
export const HIGHLIGHTED_DELAY = 10000;

View File

@ -0,0 +1,24 @@
import { MouseEventHandler } from 'react';
import { ILog } from 'types/api/logs/log';
export type LogTimeRange = {
start: number;
end: number;
pageSize: number;
};
export type UseCopyLogLink = {
isHighlighted: boolean;
isLogsExplorerPage: boolean;
activeLogId: string | null;
timeRange: LogTimeRange | null;
onLogCopy: MouseEventHandler<HTMLElement>;
onTimeRangeChange: (newTimeRange: LogTimeRange | null) => void;
};
export type UseActiveLog = {
activeLog: ILog | null;
onSetActiveLog: (log: ILog) => void;
onClearActiveLog: () => void;
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
};

View File

@ -0,0 +1,127 @@
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { SuccessResponse } from 'types/api';
import { ILog } from 'types/api/logs/log';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ILogsReducer } from 'types/reducer/logs';
import { v4 as uuid } from 'uuid';
import { UseActiveLog } from './types';
export const useActiveLog = (): UseActiveLog => {
const dispatch = useDispatch();
const {
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const queryClient = useQueryClient();
const { pathname } = useLocation();
const history = useHistory();
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const isLogsPage = useMemo(() => pathname === ROUTES.LOGS, [pathname]);
const [activeLog, setActiveLog] = useState<ILog | null>(null);
const onSetDetailedLogData = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const onSetActiveLog = useCallback(
(nextActiveLog: ILog): void => {
if (isLogsPage) {
onSetDetailedLogData(nextActiveLog);
} else {
setActiveLog(nextActiveLog);
}
},
[isLogsPage, onSetDetailedLogData],
);
const onClearActiveLog = useCallback((): void => setActiveLog(null), []);
const onAddToQueryExplorer = useCallback(
(fieldKey: string, fieldValue: string, operator: string): void => {
const keysAutocomplete: BaseAutocompleteData[] =
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>(
[QueryBuilderKeys.GET_AGGREGATE_KEYS],
{ exact: false },
)?.payload.attributeKeys || [];
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
keysAutocomplete,
fieldKey,
);
const currentOperator =
Object.keys(OPERATORS).find((op) => op === operator) || '';
const nextQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filters: {
...item.filters,
items: [
...item.filters.items.filter(
(item) => item.key?.id !== existAutocompleteKey.id,
),
{
id: uuid(),
key: existAutocompleteKey,
op: currentOperator,
value: fieldValue,
},
],
},
})),
},
};
redirectWithQueryBuilderData(nextQuery);
},
[currentQuery, queryClient, redirectWithQueryBuilderData],
);
const onAddToQueryLogs = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => {
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
operator,
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
},
[history, queryString],
);
return {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer,
};
};

View File

@ -0,0 +1,85 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData';
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { HIGHLIGHTED_DELAY } from './configs';
import { LogTimeRange, UseCopyLogLink } from './types';
export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const urlQuery = useUrlQuery();
const { pathname } = useLocation();
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const {
queryData: timeRange,
redirectWithQuery: onTimeRangeChange,
} = useUrlQueryData<LogTimeRange | null>(QueryParams.timeRange, null);
const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId,
null,
);
const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]);
const [isHighlighted, setIsHighlighted] = useState<boolean>(isActiveLog);
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const onLogCopy: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
if (!logId) return;
event.preventDefault();
event.stopPropagation();
const range = JSON.stringify(timeRange);
urlQuery.delete(QueryParams.activeLogId);
urlQuery.delete(QueryParams.timeRange);
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
urlQuery.set(QueryParams.timeRange, range);
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
notifications.success({
message: 'Copied to clipboard',
});
},
[logId, notifications, timeRange, urlQuery, pathname, setCopy],
);
useEffect(() => {
if (!isActiveLog) return;
const timer = setTimeout(() => setIsHighlighted(false), HIGHLIGHTED_DELAY);
// eslint-disable-next-line consistent-return
return (): void => {
clearTimeout(timer);
};
}, [isActiveLog]);
return {
isHighlighted,
isLogsExplorerPage,
activeLogId,
timeRange,
onLogCopy,
onTimeRangeChange,
};
};

View File

@ -16,6 +16,7 @@ export const useGetExplorerQueryRange = (
requestData: Query | null, requestData: Query | null,
panelType: PANEL_TYPES | null, panelType: PANEL_TYPES | null,
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>, options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
params?: Record<string, unknown>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => { ): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
const { isEnabledQuery } = useQueryBuilder(); const { isEnabledQuery } = useQueryBuilder();
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector< const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
@ -46,6 +47,7 @@ export const useGetExplorerQueryRange = (
selectedTime: 'GLOBAL_TIME', selectedTime: 'GLOBAL_TIME',
globalSelectedInterval, globalSelectedInterval,
query: requestData || initialQueriesMap.metrics, query: requestData || initialQueriesMap.metrics,
params,
}, },
{ {
...options, ...options,

View File

@ -14,8 +14,7 @@ import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA, SET_LOGS_ORDER } from 'types/actions/logs'; import { SET_LOGS_ORDER } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import { import {
@ -33,16 +32,6 @@ function Logs(): JSX.Element {
const { order } = useSelector<AppState, ILogsReducer>((store) => store.logs); const { order } = useSelector<AppState, ILogsReducer>((store) => store.logs);
const location = useLocation(); const location = useLocation();
const showExpandedLog = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const { const {
viewModeOptionList, viewModeOptionList,
viewModeOption, viewModeOption,
@ -141,11 +130,7 @@ function Logs(): JSX.Element {
</Col> </Col>
</Row> </Row>
<LogsTable <LogsTable viewMode={viewMode} linesPerRow={linesPerRow} />
viewMode={viewMode}
linesPerRow={linesPerRow}
onClickExpand={showExpandedLog}
/>
</Col> </Col>
</Row> </Row>

View File

@ -7,9 +7,10 @@ export interface ErrorResponse {
message: null; message: null;
} }
export interface SuccessResponse<T> { export interface SuccessResponse<T, P = unknown> {
statusCode: SuccessStatusCode; statusCode: SuccessStatusCode;
message: string; message: string;
payload: T; payload: T;
error: null; error: null;
params?: P;
} }

View File

@ -8,7 +8,7 @@ export interface ILog {
severityText: string; severityText: string;
severityNumber: number; severityNumber: number;
body: string; body: string;
resourcesString: Record<string, never>; resources_string: Record<string, never>;
attributesString: Record<string, never>; attributesString: Record<string, never>;
attributesInt: Record<string, never>; attributesInt: Record<string, never>;
attributesFloat: Record<string, never>; attributesFloat: Record<string, never>;

View File

@ -0,0 +1,14 @@
import colorAlpha from 'color-alpha';
type GetAlphaColor = Record<0 | 10 | 25 | 45 | 75 | 100, string>;
const getAlphaColor = (color: string): GetAlphaColor => ({
0: colorAlpha(color, 0),
10: colorAlpha(color, 0.1),
25: colorAlpha(color, 0.25),
45: colorAlpha(color, 0.45),
75: colorAlpha(color, 0.75),
100: colorAlpha(color, 1),
});
export default getAlphaColor;

View File

@ -4226,6 +4226,13 @@ collection-visit@^1.0.0:
map-visit "^1.0.0" map-visit "^1.0.0"
object-visit "^1.0.0" object-visit "^1.0.0"
color-alpha@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-alpha/-/color-alpha-1.1.3.tgz#71250189e9f02bba8261a94d5e7d5f5606d1749a"
integrity sha512-krPYBO1RSO5LH4AGb/b6z70O1Ip2o0F0+0cVFN5FN99jfQtZFT08rQyg+9oOBNJYAn3SRwJIFC8jUEOKz7PisA==
dependencies:
color-parse "^1.4.1"
color-convert@^1.9.0: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
@ -4250,6 +4257,13 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-parse@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-1.4.2.tgz#78651f5d34df1a57f997643d86f7f87268ad4eb5"
integrity sha512-RI7s49/8yqDj3fECFZjUI1Yi0z/Gq1py43oNJivAIIDSyJiOZLfYCRQEgn8HEVAj++PcRe8AnL2XF0fRJ3BTnA==
dependencies:
color-name "^1.0.0"
color-string@^1.9.0: color-string@^1.9.0:
version "1.9.1" version "1.9.1"
resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz"