fix: exporer log details action buttons (#3126)

* fix: exporer log details action buttons

* chore: magic strings is removed

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Yevhen Shevchenko 2023-07-13 15:55:43 +03:00 committed by GitHub
parent 60c0836d3e
commit d26022efb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 244 additions and 188 deletions

View File

@ -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<AddToQueryHOCProps, 'onAddToQuery'>;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'>;

View File

@ -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 && <TableView logData={log} onAddToQuery={onAddToQuery} />,
children: log && (
<TableView
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
/>
),
},
{
label: 'JSON',

View File

@ -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(() => <span>Add to query: {fieldKey}</span>, [
@ -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;
}

View File

@ -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<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
}
function ActionItem({
fieldKey,
fieldValue,
getLogs,
getLogsAggregate,
}: ActionItemProps): JSX.Element | unknown {
const {
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch<Dispatch<AppActions>>();
onClickActionItem,
}: ActionItemProps): JSX.Element {
const handleClick = useCallback(
(operator: string) => {
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(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(
() => (
<Col>
<Button
type="text"
size="small"
onClick={(): void =>
handleQueryAdd(
generateFilterQuery({
fieldKey,
fieldValue: validatedFieldValue,
type: 'IN',
}),
)
}
>
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
<PlusCircleOutlined /> Filter for value
</Button>
<br />
<Button
type="text"
size="small"
onClick={(): void =>
handleQueryAdd(
generateFilterQuery({
fieldKey,
fieldValue: validatedFieldValue,
type: 'NIN',
}),
)
}
>
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
<MinusCircleOutlined /> Filter out value
</Button>
</Col>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[fieldKey, validatedFieldValue],
[onClickHandler],
);
return (
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
@ -152,19 +47,15 @@ function ActionItem({
</Popover>
);
}
interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: never) => void;
export interface ActionItemProps {
fieldKey: string;
fieldValue: string;
onClickActionItem: (
fieldKey: string,
fieldValue: string,
operator: string,
) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): 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);

View File

@ -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<AddToQueryHOCProps, 'onAddToQuery'>;
type Props = TableViewProps &
Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'>;
function TableView({ logData, onAddToQuery }: Props): JSX.Element | null {
function TableView({
logData,
onAddToQuery,
onClickActionItem,
}: Props): JSX.Element | null {
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const dispatch = useDispatch<Dispatch<AppActions>>();
@ -89,7 +95,13 @@ function TableView({ logData, onAddToQuery }: Props): JSX.Element | null {
render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return <ActionItem fieldKey={fieldKey} fieldValue={fieldData.value} />;
return (
<ActionItem
fieldKey={fieldKey[0]}
fieldValue={fieldData.value}
onClickActionItem={onClickActionItem}
/>
);
}
return null;
},

View File

@ -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<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
};
function LogDetailedView({
getLogs,
getLogsAggregate,
}: LogDetailedViewProps): JSX.Element {
const history = useHistory();
const {
detailedLog,
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
@ -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 (
<LogDetail
log={detailedLog}
onClose={onDrawerClose}
onAddToQuery={handleQueryAdd}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleClickActionItem}
/>
);
}
export default LogDetailedView;
interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: never) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): 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));

View File

@ -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<SuccessResponse<IQueryAutocompleteResponse>>(
[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 {
<LogDetail
log={activeLog}
onClose={handleClearActiveLog}
onAddToQuery={handleAddQuery}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
/>
</>
);

View File

@ -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,
],
);

View File

@ -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}`;
}),

View File

@ -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;
};

View File

@ -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;
};