From d26022efb1b47ee12e149ef940fd418995ddd313 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Thu, 13 Jul 2023 15:55:43 +0300 Subject: [PATCH] fix: exporer log details action buttons (#3126) * fix: exporer log details action buttons * chore: magic strings is removed --------- Co-authored-by: Palash Gupta --- .../LogDetail/LogDetail.interfaces.ts | 4 +- frontend/src/components/LogDetail/index.tsx | 9 +- .../src/components/Logs/AddToQueryHOC.tsx | 5 +- .../container/LogDetailedView/ActionItem.tsx | 169 ++++-------------- .../container/LogDetailedView/TableView.tsx | 20 ++- .../src/container/LogDetailedView/index.tsx | 143 +++++++++++++-- .../src/container/LogsExplorerViews/index.tsx | 17 +- frontend/src/container/LogsTable/index.tsx | 23 +-- frontend/src/hooks/queryBuilder/useTag.ts | 8 +- .../src/lib/getGeneratedFilterQueryString.ts | 24 +++ frontend/src/lib/removeJSONStringifyQuotes.ts | 10 ++ 11 files changed, 244 insertions(+), 188 deletions(-) create mode 100644 frontend/src/lib/getGeneratedFilterQueryString.ts create mode 100644 frontend/src/lib/removeJSONStringifyQuotes.ts diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2e1597f905..198e2abdcd 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -1,7 +1,9 @@ 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; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 6fb43b1218..f8a320e8e9 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -8,6 +8,7 @@ function LogDetail({ log, onClose, onAddToQuery, + onClickActionItem, }: LogDetailProps): JSX.Element { const onDrawerClose = (): void => { onClose(); @@ -17,7 +18,13 @@ function LogDetail({ { label: 'Table', key: '1', - children: log && , + children: log && ( + + ), }, { label: 'JSON', diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index d4d1f9bd31..35fed9c92a 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,4 +1,5 @@ import { Popover } from 'antd'; +import { OPERATORS } from 'constants/queryBuilder'; import { memo, ReactNode, useCallback, useMemo } from 'react'; import { ButtonContainer } from './styles'; @@ -10,7 +11,7 @@ function AddToQueryHOC({ children, }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = useCallback(() => { - onAddToQuery(fieldKey, fieldValue); + onAddToQuery(fieldKey, fieldValue, OPERATORS.IN); }, [fieldKey, fieldValue, onAddToQuery]); const popOverContent = useMemo(() => Add to query: {fieldKey}, [ @@ -29,7 +30,7 @@ function AddToQueryHOC({ export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; - onAddToQuery: (fieldKey: string, fieldValue: string) => void; + onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; children: ReactNode; } diff --git a/frontend/src/container/LogDetailedView/ActionItem.tsx b/frontend/src/container/LogDetailedView/ActionItem.tsx index 2d2350a324..8c476c2c99 100644 --- a/frontend/src/container/LogDetailedView/ActionItem.tsx +++ b/frontend/src/container/LogDetailedView/ActionItem.tsx @@ -1,148 +1,43 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Col, Popover } from 'antd'; -import getStep from 'lib/getStep'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; -import { getIdConditions } from 'pages/Logs/utils'; -import { memo, useMemo } from 'react'; -import { connect, useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { getLogs } from 'store/actions/logs/getLogs'; -import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { ILogsReducer } from 'types/reducer/logs'; +import { OPERATORS } from 'constants/queryBuilder'; +import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; +import { memo, useCallback, useMemo } from 'react'; -const removeJSONStringifyQuotes = (s: string): string => { - if (!s || !s.length) { - return s; - } - - if (s[0] === '"' && s[s.length - 1] === '"') { - return s.slice(1, s.length - 1); - } - return s; -}; - -interface ActionItemProps { - fieldKey: string; - fieldValue: string; - getLogs: (props: Parameters[0]) => ReturnType; - getLogsAggregate: ( - props: Parameters[0], - ) => ReturnType; -} function ActionItem({ fieldKey, fieldValue, - getLogs, - getLogsAggregate, -}: ActionItemProps): JSX.Element | unknown { - const { - searchFilter: { queryString }, - logLinesPerPage, - idStart, - liveTail, - idEnd, - order, - } = useSelector((store) => store.logs); - const dispatch = useDispatch>(); + onClickActionItem, +}: ActionItemProps): JSX.Element { + const handleClick = useCallback( + (operator: string) => { + const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); - const { maxTime, minTime } = useSelector( - (state) => state.globalTime, + onClickActionItem(fieldKey, validatedFieldValue, operator); + }, + [onClickActionItem, fieldKey, fieldValue], ); - const handleQueryAdd = (newQueryString: string): void => { - let updatedQueryString = queryString || ''; + const onClickHandler = useCallback( + (operator: string) => (): void => { + handleClick(operator); + }, + [handleClick], + ); - if (updatedQueryString.length === 0) { - updatedQueryString += `${newQueryString}`; - } else { - updatedQueryString += ` AND ${newQueryString}`; - } - dispatch({ - type: SET_SEARCH_QUERY_STRING, - payload: { - searchQueryString: updatedQueryString, - }, - }); - - if (liveTail === 'STOPPED') { - getLogs({ - q: updatedQueryString, - limit: logLinesPerPage, - orderBy: 'timestamp', - order, - timestampStart: minTime, - timestampEnd: maxTime, - ...getIdConditions(idStart, idEnd, order), - }); - getLogsAggregate({ - timestampStart: minTime, - timestampEnd: maxTime, - step: getStep({ - start: minTime, - end: maxTime, - inputFormat: 'ns', - }), - q: updatedQueryString, - }); - } else if (liveTail === 'PLAYING') { - dispatch({ - type: TOGGLE_LIVE_TAIL, - payload: 'PAUSED', - }); - setTimeout( - () => - dispatch({ - type: TOGGLE_LIVE_TAIL, - payload: liveTail, - }), - 0, - ); - } - }; - const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); const PopOverMenuContent = useMemo( () => ( -
- ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [fieldKey, validatedFieldValue], + [onClickHandler], ); return ( @@ -152,19 +47,15 @@ function ActionItem({ ); } -interface DispatchProps { - getLogs: (props: Parameters[0]) => (dispatch: never) => void; - getLogsAggregate: ( - props: Parameters[0], - ) => (dispatch: never) => void; + +export interface ActionItemProps { + fieldKey: string; + fieldValue: string; + onClickActionItem: ( + fieldKey: string, + fieldValue: string, + operator: string, + ) => void; } -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getLogs: bindActionCreators(getLogs, dispatch), - getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default connect(null, mapDispatchToProps)(memo(ActionItem as any)); +export default memo(ActionItem); diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index b027c0e523..0d861bcebe 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -20,7 +20,7 @@ import AppActions from 'types/actions'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { ILog } from 'types/api/logs/log'; -import ActionItem from './ActionItem'; +import ActionItem, { ActionItemProps } from './ActionItem'; import { flattenObject, recursiveParseJSON } from './utils'; // Fields which should be restricted from adding it to query @@ -30,9 +30,15 @@ interface TableViewProps { logData: ILog; } -type Props = TableViewProps & Pick; +type Props = TableViewProps & + Pick & + Pick; -function TableView({ logData, onAddToQuery }: Props): JSX.Element | null { +function TableView({ + logData, + onAddToQuery, + onClickActionItem, +}: Props): JSX.Element | null { const [fieldSearchInput, setFieldSearchInput] = useState(''); const dispatch = useDispatch>(); @@ -89,7 +95,13 @@ function TableView({ logData, onAddToQuery }: Props): JSX.Element | null { render: (fieldData: Record): JSX.Element | null => { const fieldKey = fieldData.field.split('.').slice(-1); if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { - return ; + return ( + + ); } return null; }, diff --git a/frontend/src/container/LogDetailedView/index.tsx b/frontend/src/container/LogDetailedView/index.tsx index 25ccf8c50c..fe5b2cd3af 100644 --- a/frontend/src/container/LogDetailedView/index.tsx +++ b/frontend/src/container/LogDetailedView/index.tsx @@ -1,21 +1,49 @@ import LogDetail from 'components/LogDetail'; import ROUTES from 'constants/routes'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; -import { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString'; +import getStep from 'lib/getStep'; +import { getIdConditions } from 'pages/Logs/utils'; +import { memo, useCallback } from 'react'; +import { connect, useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Dispatch } from 'redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { getLogs } from 'store/actions/logs/getLogs'; +import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; +import { + SET_DETAILED_LOG_DATA, + SET_SEARCH_QUERY_STRING, + TOGGLE_LIVE_TAIL, +} from 'types/actions/logs'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { ILogsReducer } from 'types/reducer/logs'; -function LogDetailedView(): JSX.Element { +type LogDetailedViewProps = { + getLogs: (props: Parameters[0]) => ReturnType; + getLogsAggregate: ( + props: Parameters[0], + ) => ReturnType; +}; + +function LogDetailedView({ + getLogs, + getLogsAggregate, +}: LogDetailedViewProps): JSX.Element { const history = useHistory(); const { detailedLog, searchFilter: { queryString }, + logLinesPerPage, + idStart, + liveTail, + idEnd, + order, } = useSelector((state) => state.logs); + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); const dispatch = useDispatch>(); @@ -26,32 +54,109 @@ function LogDetailedView(): JSX.Element { }); }; - const handleQueryAdd = useCallback( - (fieldKey: string, fieldValue: string) => { - const generatedQuery = generateFilterQuery({ + const handleAddToQuery = useCallback( + (fieldKey: string, fieldValue: string, operator: string) => { + const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - type: 'IN', - }); + operator, + queryString, + ); - let updatedQueryString = queryString || ''; - if (updatedQueryString.length === 0) { - updatedQueryString += `${generatedQuery}`; - } else { - updatedQueryString += ` AND ${generatedQuery}`; - } history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); }, [history, queryString], ); + const handleClickActionItem = useCallback( + (fieldKey: string, fieldValue: string, operator: string): void => { + const updatedQueryString = getGeneratedFilterQueryString( + fieldKey, + fieldValue, + operator, + queryString, + ); + + dispatch({ + type: SET_SEARCH_QUERY_STRING, + payload: { + searchQueryString: updatedQueryString, + }, + }); + + if (liveTail === 'STOPPED') { + getLogs({ + q: updatedQueryString, + limit: logLinesPerPage, + orderBy: 'timestamp', + order, + timestampStart: minTime, + timestampEnd: maxTime, + ...getIdConditions(idStart, idEnd, order), + }); + getLogsAggregate({ + timestampStart: minTime, + timestampEnd: maxTime, + step: getStep({ + start: minTime, + end: maxTime, + inputFormat: 'ns', + }), + q: updatedQueryString, + }); + } else if (liveTail === 'PLAYING') { + dispatch({ + type: TOGGLE_LIVE_TAIL, + payload: 'PAUSED', + }); + setTimeout( + () => + dispatch({ + type: TOGGLE_LIVE_TAIL, + payload: liveTail, + }), + 0, + ); + } + }, + [ + dispatch, + getLogs, + getLogsAggregate, + idEnd, + idStart, + liveTail, + logLinesPerPage, + maxTime, + minTime, + order, + queryString, + ], + ); + return ( ); } -export default LogDetailedView; +interface DispatchProps { + getLogs: (props: Parameters[0]) => (dispatch: never) => void; + getLogsAggregate: ( + props: Parameters[0], + ) => (dispatch: never) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getLogs: bindActionCreators(getLogs, dispatch), + getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any)); diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 580b80e200..e626f71d8c 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -5,6 +5,7 @@ import TabLabel from 'components/TabLabel'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, + OPERATORS, PANEL_TYPES, QueryBuilderKeys, } from 'constants/queryBuilder'; @@ -226,8 +227,8 @@ function LogsExplorerViews(): JSX.Element { [currentStagedQueryData, orderByTimestamp], ); - const handleAddQuery = useCallback( - (fieldKey: string, fieldValue: string): void => { + const handleAddToQuery = useCallback( + (fieldKey: string, fieldValue: string, operator: string): void => { const keysAutocomplete: BaseAutocompleteData[] = queryClient.getQueryData>( [QueryBuilderKeys.GET_AGGREGATE_KEYS], @@ -239,6 +240,9 @@ function LogsExplorerViews(): JSX.Element { fieldKey, ); + const currentOperator = + Object.keys(OPERATORS).find((op) => op === operator) || ''; + const nextQuery: Query = { ...currentQuery, builder: { @@ -254,7 +258,7 @@ function LogsExplorerViews(): JSX.Element { { id: uuid(), key: existAutocompleteKey, - op: '=', + op: currentOperator, value: fieldValue, }, ], @@ -422,7 +426,7 @@ function LogsExplorerViews(): JSX.Element { onOpenDetailedView={handleSetActiveLog} onEndReached={handleEndReached} onExpand={handleSetActiveLog} - onAddToQuery={handleAddQuery} + onAddToQuery={handleAddToQuery} /> ), }, @@ -453,7 +457,7 @@ function LogsExplorerViews(): JSX.Element { logs, handleSetActiveLog, handleEndReached, - handleAddQuery, + handleAddToQuery, data, isError, ], @@ -506,7 +510,8 @@ function LogsExplorerViews(): JSX.Element { ); diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 94a4ee9d6c..73fdaa8b4b 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -7,7 +7,7 @@ import Spinner from 'components/Spinner'; import ROUTES from 'constants/routes'; import { contentStyle } from 'container/Trace/Search/config'; import useFontFaceObserver from 'hooks/useFontObserver'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; +import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -77,20 +77,15 @@ function LogsTable(props: LogsTableProps): JSX.Element { [dispatch], ); - const handleQueryAdd = useCallback( - (fieldKey: string, fieldValue: string) => { - const generatedQuery = generateFilterQuery({ + const handleAddToQuery = useCallback( + (fieldKey: string, fieldValue: string, operator: string) => { + const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - type: 'IN', - }); + operator, + queryString, + ); - let updatedQueryString = queryString || ''; - if (updatedQueryString.length === 0) { - updatedQueryString += `${generatedQuery}`; - } else { - updatedQueryString += ` AND ${generatedQuery}`; - } history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); }, [history, queryString], @@ -117,7 +112,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { logData={log} selectedFields={selected} onOpenDetailedView={handleOpenDetailedView} - onAddToQuery={handleQueryAdd} + onAddToQuery={handleAddToQuery} /> ); }, @@ -128,7 +123,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { linesPerRow, onClickExpand, handleOpenDetailedView, - handleQueryAdd, + handleAddToQuery, ], ); diff --git a/frontend/src/hooks/queryBuilder/useTag.ts b/frontend/src/hooks/queryBuilder/useTag.ts index 0a00c81a4c..3bc272fc86 100644 --- a/frontend/src/hooks/queryBuilder/useTag.ts +++ b/frontend/src/hooks/queryBuilder/useTag.ts @@ -34,8 +34,12 @@ export const useTag = ( () => (query?.filters?.items || []).map((ele) => { if (isInNInOperator(getOperatorFromValue(ele.op))) { - const csvString = Papa.unparse([ele.value]); - return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${csvString}`; + try { + const csvString = Papa.unparse([ele.value]); + return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${csvString}`; + } catch { + return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`; + } } return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`; }), diff --git a/frontend/src/lib/getGeneratedFilterQueryString.ts b/frontend/src/lib/getGeneratedFilterQueryString.ts new file mode 100644 index 0000000000..ea737c0b14 --- /dev/null +++ b/frontend/src/lib/getGeneratedFilterQueryString.ts @@ -0,0 +1,24 @@ +import { generateFilterQuery } from './logs/generateFilterQuery'; + +export const getGeneratedFilterQueryString = ( + fieldKey: string, + fieldValue: string, + operator: string, + queryString: string, +): string => { + let updatedQueryString = queryString || ''; + + const generatedString = generateFilterQuery({ + fieldKey, + fieldValue, + type: operator, + }); + + if (updatedQueryString.length === 0) { + updatedQueryString += `${generatedString}`; + } else { + updatedQueryString += ` AND ${generatedString}`; + } + + return updatedQueryString; +}; diff --git a/frontend/src/lib/removeJSONStringifyQuotes.ts b/frontend/src/lib/removeJSONStringifyQuotes.ts new file mode 100644 index 0000000000..5dd8c7af50 --- /dev/null +++ b/frontend/src/lib/removeJSONStringifyQuotes.ts @@ -0,0 +1,10 @@ +export const removeJSONStringifyQuotes = (s: string): string => { + if (!s || !s.length) { + return s; + } + + if (s[0] === '"' && s[s.length - 1] === '"') { + return s.slice(1, s.length - 1); + } + return s; +};