mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 21:06:00 +08:00
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:
parent
bc4a4edc7f
commit
5f89e84eaf
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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'>;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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)`
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -16,4 +16,6 @@ export enum QueryParams {
|
||||
widgetId = 'widgetId',
|
||||
order = 'order',
|
||||
q = 'q',
|
||||
activeLogId = 'activeLogId',
|
||||
timeRange = 'timeRange',
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ const themeColors = {
|
||||
whiteCream: '#ffffffd5',
|
||||
white: '#ffffff',
|
||||
black: '#000000',
|
||||
darkGrey: '#262626',
|
||||
lightBlack: '#141414',
|
||||
lightgrey: '#ddd',
|
||||
lightWhite: '#ffffffd9',
|
||||
|
36
frontend/src/container/LogsContextList/ShowButton.tsx
Normal file
36
frontend/src/container/LogsContextList/ShowButton.tsx
Normal 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;
|
9
frontend/src/container/LogsContextList/configs.ts
Normal file
9
frontend/src/container/LogsContextList/configs.ts
Normal 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,
|
||||
});
|
198
frontend/src/container/LogsContextList/index.tsx
Normal file
198
frontend/src/container/LogsContextList/index.tsx
Normal 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);
|
25
frontend/src/container/LogsContextList/styles.ts
Normal file
25
frontend/src/container/LogsContextList/styles.ts
Normal 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%);
|
||||
`;
|
52
frontend/src/container/LogsContextList/utils.ts
Normal file
52
frontend/src/container/LogsContextList/utils.ts
Normal 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;
|
||||
};
|
109
frontend/src/container/LogsExplorerContext/index.tsx
Normal file
109
frontend/src/container/LogsExplorerContext/index.tsx
Normal 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);
|
30
frontend/src/container/LogsExplorerContext/styles.ts
Normal file
30
frontend/src/container/LogsExplorerContext/styles.ts
Normal 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]};
|
||||
`;
|
6
frontend/src/container/LogsExplorerContext/types.ts
Normal file
6
frontend/src/container/LogsExplorerContext/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface LogsExplorerContextProps {
|
||||
log: ILog;
|
||||
onClose: VoidFunction;
|
||||
}
|
@ -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;
|
22
frontend/src/container/LogsExplorerContext/utils.ts
Normal file
22
frontend/src/container/LogsExplorerContext/utils.ts
Normal 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,
|
||||
};
|
||||
});
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const infinityDefaultStyles: CSSProperties = {
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
overflowX: 'scroll',
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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'>;
|
||||
};
|
||||
|
@ -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 (
|
||||
|
@ -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 />
|
||||
</>
|
||||
|
@ -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" />;
|
||||
|
1
frontend/src/hooks/logs/configs.ts
Normal file
1
frontend/src/hooks/logs/configs.ts
Normal file
@ -0,0 +1 @@
|
||||
export const HIGHLIGHTED_DELAY = 10000;
|
24
frontend/src/hooks/logs/types.ts
Normal file
24
frontend/src/hooks/logs/types.ts
Normal 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;
|
||||
};
|
127
frontend/src/hooks/logs/useActiveLog.ts
Normal file
127
frontend/src/hooks/logs/useActiveLog.ts
Normal 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,
|
||||
};
|
||||
};
|
85
frontend/src/hooks/logs/useCopyLogLink.ts
Normal file
85
frontend/src/hooks/logs/useCopyLogLink.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>;
|
||||
|
14
frontend/src/utils/getAlphaColor.ts
Normal file
14
frontend/src/utils/getAlphaColor.ts
Normal 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;
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user