feat: add support for group by attribute in log details (#5753)

* feat: add support for group by attribute in log details

* feat: auto shift to qb from search on adding groupBY

* feat: update icon and styles
This commit is contained in:
Vikrant Gupta 2024-08-22 23:59:22 +05:30 committed by GitHub
parent 96b81817e0
commit ab1caf13fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 379 additions and 92 deletions

View File

@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4344_1236)" stroke="#C0C1C3" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583"/><path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z"/></g><defs><clipPath id="prefix__clip0_4344_1236"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@ -0,0 +1,27 @@
import { Color } from '@signozhq/design-tokens';
import { useIsDarkMode } from 'hooks/useDarkMode';
function GroupByIcon(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g
clipPath="url(#prefix__clip0_4344_1236)"
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
strokeWidth="1.167"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583" />
<path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z" />
</g>
<defs>
<clipPath id="prefix__clip0_4344_1236">
<path fill="#fff" d="M0 0h14v14H0z" />
</clipPath>
</defs>
</svg>
);
}
export default GroupByIcon;

View File

@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants'; import { VIEWS } from './constants';
export type LogDetailProps = { export type LogDetailProps = {
log: ILog | null; log: ILog | null;
selectedTab: VIEWS; selectedTab: VIEWS;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
isListViewPanel?: boolean; isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null; listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> & } & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@ -37,6 +37,7 @@ function LogDetail({
log, log,
onClose, onClose,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onClickActionItem, onClickActionItem,
selectedTab, selectedTab,
isListViewPanel = false, isListViewPanel = false,
@ -209,6 +210,7 @@ function LogDetail({
logData={log} logData={log}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem} onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel} isListViewPanel={isListViewPanel}
selectedOptions={options} selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields} listViewPanelSelectedFields={listViewPanelSelectedFields}

View File

@ -141,6 +141,7 @@ function ListLogView({
onAddToQuery: handleAddToQuery, onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog, onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog, onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -257,6 +258,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery} onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT} selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog} onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
/> />
)} )}
</> </>

View File

@ -55,6 +55,7 @@ function RawLogView({
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false); const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
@ -202,6 +203,7 @@ function RawLogView({
onClose={handleCloseLogDetail} onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/> />
)} )}
</RawLogViewContainer> </RawLogViewContainer>

View File

@ -38,6 +38,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
activeLog, activeLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onSetActiveLog, onSetActiveLog,
} = useActiveLog(); } = useActiveLog();
@ -151,6 +152,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
log={activeLog} log={activeLog}
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
/> />
</> </>

View File

@ -18,6 +18,7 @@ import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem'; import { ActionItemProps } from './ActionItem';
import TableView from './TableView'; import TableView from './TableView';
@ -27,6 +28,11 @@ interface OverviewProps {
isListViewPanel?: boolean; isListViewPanel?: boolean;
selectedOptions: OptionsQuery; selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null; listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
} }
type Props = OverviewProps & type Props = OverviewProps &
@ -39,6 +45,7 @@ function Overview({
onClickActionItem, onClickActionItem,
isListViewPanel = false, isListViewPanel = false,
selectedOptions, selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields, listViewPanelSelectedFields,
}: Props): JSX.Element { }: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true); const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
@ -204,6 +211,7 @@ function Overview({
logData={logData} logData={logData}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput} fieldSearchInput={fieldSearchInput}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onClickActionItem} onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel} isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
@ -222,6 +230,7 @@ function Overview({
Overview.defaultProps = { Overview.defaultProps = {
isListViewPanel: false, isListViewPanel: false,
listViewPanelSelectedFields: null, listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
}; };
export default Overview; export default Overview;

View File

@ -11,7 +11,7 @@
top: 50%; top: 50%;
right: 16px; right: 16px;
transform: translateY(-50%); transform: translateY(-50%);
gap: 8px; gap: 4px;
} }
} }
} }
@ -76,8 +76,10 @@
box-shadow: none; box-shadow: none;
border-radius: 2px; border-radius: 2px;
background: var(--bg-slate-400); background: var(--bg-slate-400);
padding: 2px 3px;
height: 24px; gap: 3px;
height: 18px;
width: 20px;
} }
} }
} }

View File

@ -4,13 +4,12 @@ import './TableView.styles.scss';
import { LinkOutlined } from '@ant-design/icons'; import { LinkOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd'; import { Button, Space, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import cx from 'classnames'; import cx from 'classnames';
import AddToQueryHOC, { import AddToQueryHOC, {
AddToQueryHOCProps, AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC'; } from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -19,8 +18,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history'; import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { isEmpty } from 'lodash-es'; import { Pin } from 'lucide-react';
import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom'; import { generatePath } from 'react-router-dom';
@ -29,17 +27,12 @@ import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log'; import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem'; import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer'; import FieldRenderer from './FieldRenderer';
import { import { TableViewActions } from './TableView/TableViewActions';
filterKeyForField, import { filterKeyForField, findKeyPath, flattenObject } from './utils';
findKeyPath,
flattenObject,
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
} from './utils';
// Fields which should be restricted from adding it to query // Fields which should be restricted from adding it to query
const RESTRICTED_FIELDS = ['timestamp']; const RESTRICTED_FIELDS = ['timestamp'];
@ -50,6 +43,11 @@ interface TableViewProps {
selectedOptions: OptionsQuery; selectedOptions: OptionsQuery;
isListViewPanel?: boolean; isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null; listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
} }
type Props = TableViewProps & type Props = TableViewProps &
@ -63,6 +61,7 @@ function TableView({
onClickActionItem, onClickActionItem,
isListViewPanel = false, isListViewPanel = false,
selectedOptions, selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields, listViewPanelSelectedFields,
}: Props): JSX.Element | null { }: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
@ -271,75 +270,17 @@ function TableView({
width: 70, width: 70,
ellipsis: false, ellipsis: false,
className: 'value-field-container attribute-value', className: 'value-field-container attribute-value',
render: (fieldData: Record<string, string>, record): JSX.Element => { render: (fieldData: Record<string, string>, record): JSX.Element => (
const textToCopy = fieldData.value.slice(1, -1); <TableViewActions
fieldData={fieldData}
if (record.field === 'body') { record={record}
const parsedBody = recursiveParseJSON(fieldData.value); isListViewPanel={isListViewPanel}
if (!isEmpty(parsedBody)) { isfilterInLoading={isfilterInLoading}
return ( isfilterOutLoading={isfilterOutLoading}
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} /> onClickHandler={onClickHandler}
); onGroupByAttribute={onGroupByAttribute}
}
}
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<div className="value-field">
<CopyClipboardHOC textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
{!isListViewPanel && (
<span className="action-btn">
<Tooltip title="Filter for value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(
OPERATORS.IN,
fieldFilterKey,
fieldData.value,
)}
/> />
</Tooltip> ),
<Tooltip title="Filter out value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(
OPERATORS.NIN,
fieldFilterKey,
fieldData.value,
)}
/>
</Tooltip>
</span>
)}
</div>
);
},
}, },
]; ];
function sortPinnedAttributes( function sortPinnedAttributes(
@ -380,9 +321,10 @@ function TableView({
TableView.defaultProps = { TableView.defaultProps = {
isListViewPanel: false, isListViewPanel: false,
listViewPanelSelectedFields: null, listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
}; };
interface DataType { export interface DataType {
key: string; key: string;
field: string; field: string;
value: string; value: string;

View File

@ -0,0 +1,61 @@
.open-popover {
&.value-field {
.action-btn {
display: flex !important;
position: absolute !important;
top: 50% !important;
right: 16px !important;
transform: translateY(-50%) !important;
gap: 4px !important;
}
}
}
.table-view-actions-content {
.ant-popover-inner {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 0px;
.group-by-clause {
display: flex;
align-items: center;
gap: 4px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
padding: 12px 18px 12px 14px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.group-by-clause:hover {
background-color: unset !important;
}
}
}
.lightMode {
.table-view-actions-content {
.ant-popover-inner {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100) !important;
.group-by-clause {
color: var(--bg-ink-400);
}
}
}
}

View File

@ -0,0 +1,156 @@
import './TableViewActions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataType } from '../TableView';
import {
filterKeyForField,
jsonToDataNodes,
recursiveParseJSON,
removeEscapeCharacters,
} from '../utils';
interface ITableViewActionsProps {
fieldData: Record<string, string>;
record: DataType;
isListViewPanel: boolean;
isfilterInLoading: boolean;
isfilterOutLoading: boolean;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
onClickHandler: (
operator: string,
fieldKey: string,
fieldValue: string,
) => () => void;
}
export function TableViewActions(
props: ITableViewActionsProps,
): React.ReactElement {
const {
fieldData,
record,
isListViewPanel,
isfilterInLoading,
isfilterOutLoading,
onClickHandler,
onGroupByAttribute,
} = props;
const { pathname } = useLocation();
// there is no option for where clause in old logs explorer and live logs page
const isOldLogsExplorerOrLiveLogsPage = useMemo(
() => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS,
[pathname],
);
const [isOpen, setIsOpen] = useState<boolean>(false);
const textToCopy = fieldData.value.slice(1, -1);
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
return (
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
);
}
}
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
<CopyClipboardHOC textToCopy={textToCopy}>
<span
style={{
color: Color.BG_SIENNA_400,
whiteSpace: 'pre-wrap',
tabSize: 4,
}}
>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
{!isListViewPanel && (
<span className="action-btn">
<Tooltip title="Filter for value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(OPERATORS.IN, fieldFilterKey, fieldData.value)}
/>
</Tooltip>
<Tooltip title="Filter out value">
<Button
className="filter-btn periscope-btn"
icon={
isfilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(OPERATORS.NIN, fieldFilterKey, fieldData.value)}
/>
</Tooltip>
{!isOldLogsExplorerOrLiveLogsPage && (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
arrow={false}
content={
<div>
<Button
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={(): Promise<void> | void =>
onGroupByAttribute?.(fieldFilterKey)
}
>
Group By Attribute
</Button>
</div>
}
rootClassName="table-view-actions-content"
trigger="hover"
placement="bottomLeft"
>
<Button
icon={<Ellipsis size={14} />}
className="filter-btn periscope-btn"
/>
</Popover>
)}
</span>
)}
</div>
);
}
TableViewActions.defaultProps = {
onGroupByAttribute: undefined,
};

View File

@ -59,6 +59,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const { dataSource, columns } = useTableView({ const { dataSource, columns } = useTableView({
@ -172,6 +173,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={handleClearActiveContextLog} onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery} onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT} selectedTab={VIEW_TYPES.CONTEXT}
onGroupByAttribute={onGroupByAttribute}
/> />
)} )}
<LogDetail <LogDetail
@ -180,6 +182,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/> />
</> </>
); );

View File

@ -51,6 +51,7 @@ function LogsExplorerList({
activeLog, activeLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onSetActiveLog, onSetActiveLog,
} = useActiveLog(); } = useActiveLog();
@ -208,6 +209,7 @@ function LogsExplorerList({
log={activeLog} log={activeLog}
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
/> />
</> </>

View File

@ -263,10 +263,7 @@ function LogsExplorerViews({
}, },
undefined, undefined,
listQueryKeyRef, listQueryKeyRef,
{ {},
...(!isEmpty(queryId) &&
selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }),
},
); );
const getRequestData = useCallback( const getRequestData = useCallback(

View File

@ -108,6 +108,7 @@ function LogsPanelComponent({
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const handleRow = useCallback( const handleRow = useCallback(
@ -244,6 +245,7 @@ function LogsPanelComponent({
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields} listViewPanelSelectedFields={widget?.selectedLogFields}
/> />

View File

@ -36,6 +36,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
activeLog, activeLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
onSetActiveLog, onSetActiveLog,
} = useActiveLog(); } = useActiveLog();
@ -130,6 +131,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
selectedTab={VIEW_TYPES.OVERVIEW} selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog} log={activeLog}
onClose={onClearActiveLog} onClose={onClearActiveLog}
onGroupByAttribute={onGroupByAttribute}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
/> />

View File

@ -13,6 +13,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery, onAddToQuery,
onGroupByAttribute,
} = useActiveLog(); } = useActiveLog();
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log); const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
@ -42,6 +43,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onClose={onClearActiveLog} onClose={onClearActiveLog}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery} onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/> />
</div> </div>
); );

View File

@ -28,4 +28,9 @@ export type UseActiveLog = {
isJSON?: boolean, isJSON?: boolean,
dataType?: DataTypes, dataType?: DataTypes,
) => void; ) => void;
onGroupByAttribute: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
}; };

View File

@ -128,6 +128,54 @@ export const useActiveLog = (): UseActiveLog => {
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData], [currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
); );
const onGroupByAttribute = useCallback(
async (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
): Promise<void> => {
try {
const keysAutocompleteResponse = await queryClient.fetchQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey],
// eslint-disable-next-line sonarjs/no-identical-functions
async () =>
getAggregateKeys({
searchText: fieldKey,
aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute:
currentQuery.builder.queryData[0].aggregateAttribute.key,
}),
);
const keysAutocomplete: BaseAutocompleteData[] =
keysAutocompleteResponse.payload?.attributeKeys || [];
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
keysAutocomplete,
fieldKey,
isJSON,
dataType,
);
const nextQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
groupBy: [...item.groupBy, existAutocompleteKey],
})),
},
};
redirectWithQueryBuilderData(nextQuery);
} catch {
notifications.error({ message: SOMETHING_WENT_WRONG });
}
},
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
);
const onAddToQueryLogs = useCallback( const onAddToQueryLogs = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => { (fieldKey: string, fieldValue: string, operator: string) => {
const updatedQueryString = getGeneratedFilterQueryString( const updatedQueryString = getGeneratedFilterQueryString(
@ -147,5 +195,6 @@ export const useActiveLog = (): UseActiveLog => {
onSetActiveLog, onSetActiveLog,
onClearActiveLog, onClearActiveLog,
onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer, onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer,
onGroupByAttribute,
}; };
}; };

View File

@ -42,7 +42,13 @@ function LogsExplorer(): JSX.Element {
if (currentQuery.builder.queryData.length > 1) { if (currentQuery.builder.queryData.length > 1) {
handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER);
} }
}, [currentQuery.builder.queryData.length]); if (
currentQuery.builder.queryData.length === 1 &&
currentQuery.builder.queryData[0].groupBy.length > 0
) {
handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER);
}
}, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]);
const isMultipleQueries = useMemo( const isMultipleQueries = useMemo(
() => () =>
@ -51,12 +57,19 @@ function LogsExplorer(): JSX.Element {
[currentQuery], [currentQuery],
); );
const isGroupByPresent = useMemo(
() =>
currentQuery.builder.queryData.length === 1 &&
currentQuery.builder.queryData[0].groupBy.length > 0,
[currentQuery.builder.queryData],
);
const toolbarViews = useMemo( const toolbarViews = useMemo(
() => ({ () => ({
search: { search: {
name: 'search', name: 'search',
label: 'Search', label: 'Search',
disabled: isMultipleQueries, disabled: isMultipleQueries || isGroupByPresent,
show: true, show: true,
}, },
queryBuilder: { queryBuilder: {
@ -72,7 +85,7 @@ function LogsExplorer(): JSX.Element {
show: false, show: false,
}, },
}), }),
[isMultipleQueries], [isGroupByPresent, isMultipleQueries],
); );
return ( return (