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-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
"color-alpha": "1.1.3",
"cross-env": "^7.0.3",
"css-loader": "4.3.0",
"css-minimizer-webpack-plugin": "^3.2.0",

View File

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

View File

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

View File

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

View File

@ -1,9 +1,19 @@
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 { Button, Divider, Row, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
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';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
@ -85,24 +95,40 @@ function LogSelectedField({
type ListLogViewProps = {
logData: ILog;
onOpenDetailedView: (log: ILog) => void;
selectedFields: IField[];
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;
};
function ListLogView({
logData,
selectedFields,
onOpenDetailedView,
onAddToQuery,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const isDarkMode = useIsDarkMode();
const [, setCopy] = useCopyToClipboard();
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(() => {
onOpenDetailedView(logData);
}, [logData, onOpenDetailedView]);
onSetActiveLog(logData);
}, [logData, onSetActiveLog]);
const handleShowContext = useCallback(() => {
handleSetActiveContextLog(logData);
}, [logData, handleSetActiveContextLog]);
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
@ -125,7 +151,7 @@ function ListLogView({
);
return (
<Container>
<Container $isActiveLog={isHighlighted} $isDarkMode={isDarkMode}>
<div>
<LogContainer>
<>
@ -169,6 +195,42 @@ function ListLogView({
>
Copy JSON
</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>
</Container>
);

View File

@ -1,12 +1,26 @@
import { Card, Typography } from 'antd';
import { themeColors } from 'constants/theme';
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;
margin-bottom: 0.3rem;
.ant-card-body {
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)`

View File

@ -1,16 +1,32 @@
import { ExpandAltOutlined } from '@ant-design/icons';
// const Convert = require('ansi-to-html');
import {
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
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 dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useCallback, useMemo } from 'react';
import {
KeyboardEvent,
MouseEvent,
MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
// interfaces
import { ILog } from 'types/api/logs/log';
// styles
import {
ActionButtonsWrapper,
ExpandIconWrapper,
RawLogContent,
RawLogViewContainer,
@ -19,15 +35,34 @@ import {
const convert = new Convert();
interface RawLogViewProps {
isActiveLog?: boolean;
isReadOnly?: boolean;
data: ILog;
linesPerRow: number;
onClickExpand: (log: ILog) => void;
}
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 isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
const text = useMemo(
() =>
@ -38,8 +73,43 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
);
const handleClickExpand = useCallback(() => {
onClickExpand(data);
}, [onClickExpand, data]);
if (activeContextLog || isReadOnly) return;
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(
() => ({
@ -48,19 +118,69 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
[text],
);
const mouseActions = useMemo(
() => ({ onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }),
[handleMouseEnter, handleMouseLeave],
);
return (
<RawLogViewContainer
onClick={handleClickExpand}
wrap={false}
align="middle"
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isActiveLog={isHighlighted}
// eslint-disable-next-line react/jsx-props-no-spreading
{...mouseActions}
>
<ExpandIconWrapper flex="30px">
<ExpandAltOutlined />
</ExpandIconWrapper>
<RawLogContent linesPerRow={linesPerRow} dangerouslySetInnerHTML={html} />
{!isReadOnly && (
<ExpandIconWrapper flex="30px">
<ExpandAltOutlined />
</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>
);
}
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,
};
export default RawLogView;

View File

@ -1,8 +1,15 @@
import { blue } from '@ant-design/colors';
import { Col, Row } from 'antd';
import { blue, orange } from '@ant-design/colors';
import { Col, Row, Space } from 'antd';
import { themeColors } from 'constants/theme';
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%;
font-weight: 700;
font-size: 0.625rem;
@ -10,10 +17,19 @@ export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>`
transition: background-color 0.2s ease-in;
&:hover {
background-color: ${({ $isDarkMode }): string =>
$isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0, 0, 0, 0.1)'};
}
${({ $isActiveLog }): string =>
$isActiveLog ? `background-color: ${orange[3]};` : ''}
${({ $isReadOnly, $isDarkMode }): string =>
!$isReadOnly
? `&:hover {
background-color: ${
$isDarkMode
? getAlphaColor(themeColors.white)[10]
: getAlphaColor(themeColors.black)[10]
};
}`
: ''}
`;
export const ExpandIconWrapper = styled(Col)`
@ -25,6 +41,8 @@ export const ExpandIconWrapper = styled(Col)`
interface RawLogContentProps {
linesPerRow: number;
$isReadOnly: boolean;
$isActiveLog: boolean;
}
export const RawLogContent = styled.div<RawLogContentProps>`
@ -42,5 +60,17 @@ export const RawLogContent = styled.div<RawLogContentProps>`
font-size: 1rem;
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;
`;

View File

@ -10,7 +10,6 @@ export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
onClickExpand: (log: ILog) => void;
};
export type UseTableViewResult = {
@ -20,4 +19,12 @@ export type UseTableViewResult = {
export type UseTableViewProps = {
appendTo?: 'center' | 'end';
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
} & 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 { Typography } from 'antd';
import { Button, Space, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { ExpandIconWrapper } from '../RawLogView/styles';
import { defaultCellStyle, defaultTableStyle } from './config';
import { TableBodyContent } from './styles';
import {
ActionsColumnProps,
ColumnTypeRender,
UseTableViewProps,
UseTableViewResult,
@ -18,19 +24,60 @@ import {
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 => {
const {
logs,
fields,
linesPerRow,
onClickExpand,
appendTo = 'center',
onOpenLogsContext,
onClickExpand,
} = props;
const { isLogsExplorerPage } = useCopyLogLink();
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const handleClickExpand = useCallback(
(index: number): void => {
if (!onClickExpand) return;
onClickExpand(logs[index]);
},
[logs, onClickExpand],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
@ -63,7 +110,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
children: (
<ExpandIconWrapper
onClick={(): void => {
onClickExpand(logs[index]);
handleClickExpand(index);
}}
>
<ExpandAltOutlined />
@ -106,8 +153,34 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}),
},
...(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 };
};

View File

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

View File

@ -38,6 +38,7 @@ const themeColors = {
whiteCream: '#ffffffd5',
white: '#ffffff',
black: '#000000',
darkGrey: '#262626',
lightBlack: '#141414',
lightgrey: '#ddd',
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 ReactDragListView from 'react-drag-listview';
import { TableComponents } from 'react-virtuoso';
@ -5,13 +6,18 @@ import { TableComponents } from 'react-virtuoso';
import { TableStyled } from './styles';
interface LogsCustomTableProps {
isLoading?: boolean;
handleDragEnd: (fromIndex: number, toIndex: number) => void;
}
export const LogsCustomTable = ({
isLoading,
handleDragEnd,
}: LogsCustomTableProps): TableComponents['Table'] =>
function CustomTable({ style, children }): JSX.Element {
if (isLoading) {
return <Spinner height="35px" tip="Getting Logs" />;
}
return (
<ReactDragListView.DragColumn
// eslint-disable-next-line react/jsx-props-no-spreading

View File

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

View File

@ -1,16 +1,27 @@
import LogDetail from 'components/LogDetail';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { useTableView } from 'components/Logs/TableView/useTableView';
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 { getDraggedColumns } from 'hooks/useDragColumns/utils';
import {
cloneElement,
forwardRef,
ReactElement,
ReactNode,
useCallback,
useMemo,
} 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 { LogsCustomTable } from './LogsCustomTable';
@ -22,105 +33,153 @@ import {
import { InfinityTableProps } from './types';
// eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents['TableRow'] = ({
const CustomTableRow: TableComponents<ILog>['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 { 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],
);
}) => {
const isDarkMode = useIsDarkMode();
const { isHighlighted } = useCopyLogLink(props.item.id);
return (
<TableVirtuoso
style={infinityDefaultStyles}
data={dataSource}
components={{
// eslint-disable-next-line react/jsx-props-no-spreading
Table: LogsCustomTable({ 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}
/>
<TableRowStyled
$isDarkMode={isDarkMode}
$isActiveLog={isHighlighted}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</TableRowStyled>
);
}
};
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;

View File

@ -22,7 +22,19 @@ export const TableCellStyled = styled.td`
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 {
${TableCellStyled} {
background-color: #1d1d1d;

View File

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

View File

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

View File

@ -1,13 +1,10 @@
import { TabsProps } from 'antd';
import LogDetail from 'components/LogDetail';
import TabLabel from 'components/TabLabel';
import { QueryParams } from 'constants/query';
import {
initialAutocompleteData,
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
QueryBuilderKeys,
} from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
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 { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
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 { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { generatePath, useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
OrderByPayload,
@ -47,7 +39,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled';
@ -55,8 +46,7 @@ function LogsExplorerViews(): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
const queryClient = useQueryClient();
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
const { queryData: pageSize } = useUrlQueryData(
queryParamNamesMap.pageSize,
DEFAULT_PER_PAGE_VALUE,
@ -79,7 +69,6 @@ function LogsExplorerViews(): JSX.Element {
} = useQueryBuilder();
// State
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);
@ -167,16 +156,16 @@ function LogsExplorerViews(): JSX.Element {
keepPreviousData: true,
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(
(newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators(
@ -245,51 +234,6 @@ function LogsExplorerViews(): JSX.Element {
[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(
(index: number) => {
if (isLimit) return;
@ -397,14 +341,24 @@ function LogsExplorerViews(): JSX.Element {
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
useEffect(() => {
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
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]);
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]);
useEffect(() => {
@ -415,14 +369,28 @@ function LogsExplorerViews(): JSX.Element {
const newRequestData = getRequestData(stagedQuery, {
page: 1,
log: null,
pageSize,
pageSize:
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
});
setLogs([]);
setPage(1);
setRequestData(newRequestData);
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(
() => [
@ -441,10 +409,7 @@ function LogsExplorerViews(): JSX.Element {
isLoading={isFetching}
currentStagedQueryData={currentStagedQueryData}
logs={logs}
onOpenDetailedView={handleSetActiveLog}
onEndReached={handleEndReached}
onExpand={handleSetActiveLog}
onAddToQuery={handleAddToQuery}
/>
),
},
@ -472,9 +437,7 @@ function LogsExplorerViews(): JSX.Element {
isFetching,
currentStagedQueryData,
logs,
handleSetActiveLog,
handleEndReached,
handleAddToQuery,
data,
isError,
],
@ -524,12 +487,6 @@ function LogsExplorerViews(): JSX.Element {
onChange={handleChangeView}
destroyInactiveTabPane
/>
<LogDetail
log={activeLog}
onClose={handleClearActiveLog}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
/>
<GoToTop />
</>

View File

@ -4,18 +4,13 @@ import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner';
import ROUTES from 'constants/routes';
import { contentStyle } from 'container/Trace/Search/config';
import useFontFaceObserver from 'hooks/useFontObserver';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
import { AppState } from 'store/reducers';
// interfaces
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs';
// styles
@ -26,15 +21,10 @@ export type LogViewMode = 'raw' | 'table' | 'list';
type LogsTableProps = {
viewMode: LogViewMode;
linesPerRow: number;
onClickExpand: (logData: ILog) => void;
};
function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, onClickExpand, linesPerRow } = props;
const history = useHistory();
const dispatch = useDispatch();
const { viewMode, linesPerRow } = props;
useFontFaceObserver(
[
@ -52,7 +42,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
const {
logs,
fields: { selected },
searchFilter: { queryString },
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
@ -67,75 +56,23 @@ function LogsTable(props: LogsTableProps): JSX.Element {
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(
(index: number): JSX.Element => {
const log = logs[index];
if (viewMode === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={linesPerRow}
onClickExpand={onClickExpand}
/>
);
return <RawLogView key={log.id} data={log} linesPerRow={linesPerRow} />;
}
return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selected}
onOpenDetailedView={handleOpenDetailedView}
onAddToQuery={handleAddToQuery}
/>
);
return <ListLogView key={log.id} logData={log} selectedFields={selected} />;
},
[
logs,
viewMode,
selected,
linesPerRow,
onClickExpand,
handleOpenDetailedView,
handleAddToQuery,
],
[logs, viewMode, selected, linesPerRow],
);
const renderContent = useMemo(() => {
if (viewMode === 'table') {
return (
<LogsTableView
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
onClickExpand={onClickExpand}
/>
<LogsTableView logs={logs} fields={selected} linesPerRow={linesPerRow} />
);
}
@ -148,7 +85,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
/>
</Card>
);
}, [getItemContent, linesPerRow, logs, onClickExpand, selected, viewMode]);
}, [getItemContent, linesPerRow, logs, selected, viewMode]);
if (isLoading) {
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,
panelType: PANEL_TYPES | null,
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
params?: Record<string, unknown>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
const { isEnabledQuery } = useQueryBuilder();
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
@ -46,6 +47,7 @@ export const useGetExplorerQueryRange = (
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval,
query: requestData || initialQueriesMap.metrics,
params,
},
{
...options,

View File

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

View File

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

View File

@ -8,7 +8,7 @@ export interface ILog {
severityText: string;
severityNumber: number;
body: string;
resourcesString: Record<string, never>;
resources_string: Record<string, never>;
attributesString: Record<string, never>;
attributesInt: 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"
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:
version "1.9.3"
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"
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:
version "1.9.1"
resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz"