mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 06:19:03 +08:00
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:
parent
96b81817e0
commit
ab1caf13fc
1
frontend/public/Icons/groupBy.svg
Normal file
1
frontend/public/Icons/groupBy.svg
Normal 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 |
27
frontend/src/assets/CustomIcons/GroupByIcon.tsx
Normal file
27
frontend/src/assets/CustomIcons/GroupByIcon.tsx
Normal 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;
|
@ -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'> &
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -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(
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user