From 5f89e84eafa7b34c299c0606783e012d4a76c77d Mon Sep 17 00:00:00 2001 From: dnazarenkoo <134951516+dnazarenkoo@users.noreply.github.com> Date: Sun, 30 Jul 2023 14:02:18 +0300 Subject: [PATCH] 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 --- frontend/package.json | 1 + frontend/src/api/metrics/getQueryRange.ts | 1 + .../LogDetail/LogDetail.interfaces.ts | 5 +- frontend/src/components/LogDetail/index.tsx | 6 +- .../src/components/Logs/ListLogView/index.tsx | 78 +++++- .../src/components/Logs/ListLogView/styles.ts | 16 +- .../src/components/Logs/RawLogView/index.tsx | 142 +++++++++- .../src/components/Logs/RawLogView/styles.ts | 44 +++- .../src/components/Logs/TableView/types.ts | 9 +- .../Logs/TableView/useTableView.tsx | 85 +++++- frontend/src/constants/query.ts | 2 + frontend/src/constants/theme.ts | 1 + .../container/LogsContextList/ShowButton.tsx | 36 +++ .../src/container/LogsContextList/configs.ts | 9 + .../src/container/LogsContextList/index.tsx | 198 ++++++++++++++ .../src/container/LogsContextList/styles.ts | 25 ++ .../src/container/LogsContextList/utils.ts | 52 ++++ .../container/LogsExplorerContext/index.tsx | 109 ++++++++ .../container/LogsExplorerContext/styles.ts | 30 +++ .../container/LogsExplorerContext/types.ts | 6 + .../LogsExplorerContext/useInitialQuery.ts | 36 +++ .../container/LogsExplorerContext/utils.ts | 22 ++ .../InfinityTableView/LogsCustomTable.tsx | 6 + .../InfinityTableView/config.ts | 2 +- .../InfinityTableView/index.tsx | 249 +++++++++++------- .../InfinityTableView/styles.ts | 14 +- .../InfinityTableView/types.ts | 3 +- .../LogsExplorerList.interfaces.ts | 5 +- .../src/container/LogsExplorerList/index.tsx | 55 ++-- .../src/container/LogsExplorerViews/index.tsx | 119 +++------ frontend/src/container/LogsTable/index.tsx | 77 +----- frontend/src/hooks/logs/configs.ts | 1 + frontend/src/hooks/logs/types.ts | 24 ++ frontend/src/hooks/logs/useActiveLog.ts | 127 +++++++++ frontend/src/hooks/logs/useCopyLogLink.ts | 85 ++++++ .../queryBuilder/useGetExplorerQueryRange.ts | 2 + frontend/src/pages/Logs/index.tsx | 19 +- frontend/src/types/api/index.ts | 3 +- frontend/src/types/api/logs/log.ts | 2 +- frontend/src/utils/getAlphaColor.ts | 14 + frontend/yarn.lock | 14 + 41 files changed, 1393 insertions(+), 341 deletions(-) create mode 100644 frontend/src/container/LogsContextList/ShowButton.tsx create mode 100644 frontend/src/container/LogsContextList/configs.ts create mode 100644 frontend/src/container/LogsContextList/index.tsx create mode 100644 frontend/src/container/LogsContextList/styles.ts create mode 100644 frontend/src/container/LogsContextList/utils.ts create mode 100644 frontend/src/container/LogsExplorerContext/index.tsx create mode 100644 frontend/src/container/LogsExplorerContext/styles.ts create mode 100644 frontend/src/container/LogsExplorerContext/types.ts create mode 100644 frontend/src/container/LogsExplorerContext/useInitialQuery.ts create mode 100644 frontend/src/container/LogsExplorerContext/utils.ts create mode 100644 frontend/src/hooks/logs/configs.ts create mode 100644 frontend/src/hooks/logs/types.ts create mode 100644 frontend/src/hooks/logs/useActiveLog.ts create mode 100644 frontend/src/hooks/logs/useCopyLogLink.ts create mode 100644 frontend/src/utils/getAlphaColor.ts diff --git a/frontend/package.json b/frontend/package.json index c6208a9a82..dd64d0bc14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/api/metrics/getQueryRange.ts b/frontend/src/api/metrics/getQueryRange.ts index 9a0a22bda7..bc70d19832 100644 --- a/frontend/src/api/metrics/getQueryRange.ts +++ b/frontend/src/api/metrics/getQueryRange.ts @@ -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); diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 198e2abdcd..a67dfc10c8 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -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 & - Pick; + Pick & + Pick; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 8ea0709fbd..b787322ca7 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -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 diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index 91d0787a95..b56614edcf 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -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; +}; 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 ( - +
<> @@ -169,6 +195,42 @@ function ListLogView({ > Copy JSON + + {isLogsExplorerPage && ( + <> + + + + )} + + {activeContextLog && ( + + )} + ); diff --git a/frontend/src/components/Logs/ListLogView/styles.ts b/frontend/src/components/Logs/ListLogView/styles.ts index 313f5b9e0c..452bb653fa 100644 --- a/frontend/src/components/Logs/ListLogView/styles.ts +++ b/frontend/src/components/Logs/ListLogView/styles.ts @@ -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)` diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 76d12c1a22..e926e73643 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -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(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 | KeyboardEvent, + ) => { + 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 = 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 ( - - - - + {!isReadOnly && ( + + + + )} + + + + {hasActionButtons && ( + + + + + ); +} + +export default ShowButton; diff --git a/frontend/src/container/LogsContextList/configs.ts b/frontend/src/container/LogsContextList/configs.ts new file mode 100644 index 0000000000..2fbb159b9d --- /dev/null +++ b/frontend/src/container/LogsContextList/configs.ts @@ -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, +}); diff --git a/frontend/src/container/LogsContextList/index.tsx b/frontend/src/container/LogsContextList/index.tsx new file mode 100644 index 0000000000..0433a5f636 --- /dev/null +++ b/frontend/src/container/LogsContextList/index.tsx @@ -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([]); + const [page, setPage] = useState(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( + initialLogsRequest, + ); + + const handleSuccess = useCallback( + (data: SuccessResponse) => { + 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 => ( + + ), + [], + ); + + return ( + <> + {order === FILTERS.ASC && ( + + )} + + + {((!logs.length && !isFetching) || isError) && ( + No Data + )} + {isFetching && } + + + + + {order === FILTERS.DESC && ( + + )} + + ); +} + +export default memo(LogsContextList); diff --git a/frontend/src/container/LogsContextList/styles.ts b/frontend/src/container/LogsContextList/styles.ts new file mode 100644 index 0000000000..85cf3128f1 --- /dev/null +++ b/frontend/src/container/LogsContextList/styles.ts @@ -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%); +`; diff --git a/frontend/src/container/LogsContextList/utils.ts b/frontend/src/container/LogsContextList/utils.ts new file mode 100644 index 0000000000..2fc832d8e9 --- /dev/null +++ b/frontend/src/container/LogsContextList/utils.ts @@ -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; +}; diff --git a/frontend/src/container/LogsExplorerContext/index.tsx b/frontend/src/container/LogsExplorerContext/index.tsx new file mode 100644 index 0000000000..746148f3d1 --- /dev/null +++ b/frontend/src/container/LogsExplorerContext/index.tsx @@ -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(initialContextQuery); + const [filters, setFilters] = useState(null); + const [isEdit, setIsEdit] = useState(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 ( + + Logs Context + + } + onClick={handleClickEditButton} + /> + + } + > + {isEdit && ( + + )} + + + + + ); +} + +export default memo(LogsExplorerContext); diff --git a/frontend/src/container/LogsExplorerContext/styles.ts b/frontend/src/container/LogsExplorerContext/styles.ts new file mode 100644 index 0000000000..2236c20d53 --- /dev/null +++ b/frontend/src/container/LogsExplorerContext/styles.ts @@ -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]}; +`; diff --git a/frontend/src/container/LogsExplorerContext/types.ts b/frontend/src/container/LogsExplorerContext/types.ts new file mode 100644 index 0000000000..343171a740 --- /dev/null +++ b/frontend/src/container/LogsExplorerContext/types.ts @@ -0,0 +1,6 @@ +import { ILog } from 'types/api/logs/log'; + +export interface LogsExplorerContextProps { + log: ILog; + onClose: VoidFunction; +} diff --git a/frontend/src/container/LogsExplorerContext/useInitialQuery.ts b/frontend/src/container/LogsExplorerContext/useInitialQuery.ts new file mode 100644 index 0000000000..7c0f49029c --- /dev/null +++ b/frontend/src/container/LogsExplorerContext/useInitialQuery.ts @@ -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; diff --git a/frontend/src/container/LogsExplorerContext/utils.ts b/frontend/src/container/LogsExplorerContext/utils.ts new file mode 100644 index 0000000000..93456ce0cd --- /dev/null +++ b/frontend/src/container/LogsExplorerContext/utils.ts @@ -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, + }; + }); diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx index d7ba10fb01..a5cc609492 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx @@ -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 ; + } return ( ['TableRow'] = ({ children, context, ...props - // eslint-disable-next-line react/jsx-props-no-spreading -}) => {children}; - -function InfinityTable({ - tableViewProps, - infitiyTableProps, -}: InfinityTableProps): JSX.Element | null { - const { onEndReached } = infitiyTableProps; - const { dataSource, columns } = useTableView(tableViewProps); - const { draggedColumns, onDragColumns } = useDragColumns< - Record - >(LOCALSTORAGE.LOGS_LIST_COLUMNS); - - const tableColumns = useMemo( - () => getDraggedColumns>(columns, draggedColumns), - [columns, draggedColumns], - ); - - const handleDragEnd = useCallback( - (fromIndex: number, toIndex: number) => - onDragColumns(tableColumns, fromIndex, toIndex), - [tableColumns, onDragColumns], - ); - - const itemContent = useCallback( - (index: number, log: Record): JSX.Element => ( - <> - {tableColumns.map((column) => { - if (!column.render) return Empty; - - const element: ColumnTypeRender> = column.render( - log[column.key as keyof Record], - log, - index, - ); - - const elementWithChildren = element as Exclude< - ColumnTypeRender>, - ReactNode - >; - - const children = elementWithChildren.children as ReactElement; - const props = elementWithChildren.props as Record; - - return ( - - {cloneElement(children, props)} - - ); - })} - - ), - [tableColumns], - ); - - const tableHeader = useCallback( - () => ( - - {tableColumns.map((column) => { - const isDragColumn = column.key !== 'expand'; - - return ( - - {column.title as string} - - ); - })} - - ), - [tableColumns], - ); +}) => { + const isDarkMode = useIsDarkMode(); + const { isHighlighted } = useCopyLogLink(props.item.id); return ( - + + {children} + ); -} +}; + +const InfinityTable = forwardRef( + 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 + >(LOCALSTORAGE.LOGS_LIST_COLUMNS); + + const tableColumns = useMemo( + () => getDraggedColumns>(columns, draggedColumns), + [columns, draggedColumns], + ); + + const handleDragEnd = useCallback( + (fromIndex: number, toIndex: number) => + onDragColumns(tableColumns, fromIndex, toIndex), + [tableColumns, onDragColumns], + ); + + const itemContent = useCallback( + (index: number, log: Record): JSX.Element => ( + <> + {tableColumns.map((column) => { + if (!column.render) return Empty; + + const element: ColumnTypeRender> = column.render( + log[column.key as keyof Record], + log, + index, + ); + + const elementWithChildren = element as Exclude< + ColumnTypeRender>, + ReactNode + >; + + const children = elementWithChildren.children as ReactElement; + const props = elementWithChildren.props as Record; + + return ( + + {cloneElement(children, props)} + + ); + })} + + ), + [tableColumns], + ); + + const tableHeader = useCallback( + () => ( + + {tableColumns.map((column) => { + const isDragColumn = column.key !== 'expand'; + + return ( + + {column.title as string} + + ); + })} + + ), + [tableColumns], + ); + + return ( + <> + + + {activeContextLog && ( + + )} + + + ); + }, +); export default InfinityTable; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts index 024ba88a9e..8fe4fd50c5 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts @@ -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; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts index bf0e5a654c..fb8eb23170 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts @@ -1,7 +1,8 @@ import { UseTableViewProps } from 'components/Logs/TableView/types'; export type InfinityTableProps = { - tableViewProps: UseTableViewProps; + isLoading?: boolean; + tableViewProps: Omit; infitiyTableProps: { onEndReached: (index: number) => void; }; diff --git a/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts b/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts index 6862fe5ee9..ba68c67eb8 100644 --- a/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts +++ b/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts @@ -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; +}; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 184e4e73f0..56c997c669 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -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(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 ( - + ); } return ( - + ); }, - [ - 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 ( (null); const [page, setPage] = useState(1); const [logs, setLogs] = useState([]); const [requestData, setRequestData] = useState(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>( - [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; 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 /> - diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 73fdaa8b4b..729b9c6b94 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -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((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 ( - - ); + return ; } - return ( - - ); + return ; }, - [ - logs, - viewMode, - selected, - linesPerRow, - onClickExpand, - handleOpenDetailedView, - handleAddToQuery, - ], + [logs, viewMode, selected, linesPerRow], ); const renderContent = useMemo(() => { if (viewMode === 'table') { return ( - + ); } @@ -148,7 +85,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { /> ); - }, [getItemContent, linesPerRow, logs, onClickExpand, selected, viewMode]); + }, [getItemContent, linesPerRow, logs, selected, viewMode]); if (isLoading) { return ; diff --git a/frontend/src/hooks/logs/configs.ts b/frontend/src/hooks/logs/configs.ts new file mode 100644 index 0000000000..12dc8d9615 --- /dev/null +++ b/frontend/src/hooks/logs/configs.ts @@ -0,0 +1 @@ +export const HIGHLIGHTED_DELAY = 10000; diff --git a/frontend/src/hooks/logs/types.ts b/frontend/src/hooks/logs/types.ts new file mode 100644 index 0000000000..3776ba606a --- /dev/null +++ b/frontend/src/hooks/logs/types.ts @@ -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; + 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; +}; diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts new file mode 100644 index 0000000000..004d7e1d92 --- /dev/null +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -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((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(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>( + [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, + }; +}; diff --git a/frontend/src/hooks/logs/useCopyLogLink.ts b/frontend/src/hooks/logs/useCopyLogLink.ts new file mode 100644 index 0000000000..81c768618f --- /dev/null +++ b/frontend/src/hooks/logs/useCopyLogLink.ts @@ -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(QueryParams.timeRange, null); + + const { queryData: activeLogId } = useUrlQueryData( + QueryParams.activeLogId, + null, + ); + + const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]); + const [isHighlighted, setIsHighlighted] = useState(isActiveLog); + + const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ + pathname, + ]); + + const onLogCopy: MouseEventHandler = 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, + }; +}; diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index 408e598727..85a588d59e 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -16,6 +16,7 @@ export const useGetExplorerQueryRange = ( requestData: Query | null, panelType: PANEL_TYPES | null, options?: UseQueryOptions, Error>, + params?: Record, ): UseQueryResult, 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, diff --git a/frontend/src/pages/Logs/index.tsx b/frontend/src/pages/Logs/index.tsx index 4810d3e00b..f9ebd04bb7 100644 --- a/frontend/src/pages/Logs/index.tsx +++ b/frontend/src/pages/Logs/index.tsx @@ -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((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 { - + diff --git a/frontend/src/types/api/index.ts b/frontend/src/types/api/index.ts index fbd8db5bf1..c2f4d44ca0 100644 --- a/frontend/src/types/api/index.ts +++ b/frontend/src/types/api/index.ts @@ -7,9 +7,10 @@ export interface ErrorResponse { message: null; } -export interface SuccessResponse { +export interface SuccessResponse { statusCode: SuccessStatusCode; message: string; payload: T; error: null; + params?: P; } diff --git a/frontend/src/types/api/logs/log.ts b/frontend/src/types/api/logs/log.ts index a7c00885ff..af498be262 100644 --- a/frontend/src/types/api/logs/log.ts +++ b/frontend/src/types/api/logs/log.ts @@ -8,7 +8,7 @@ export interface ILog { severityText: string; severityNumber: number; body: string; - resourcesString: Record; + resources_string: Record; attributesString: Record; attributesInt: Record; attributesFloat: Record; diff --git a/frontend/src/utils/getAlphaColor.ts b/frontend/src/utils/getAlphaColor.ts new file mode 100644 index 0000000000..b57417a1ae --- /dev/null +++ b/frontend/src/utils/getAlphaColor.ts @@ -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; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index de53c86183..f1f6cfbe71 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"