diff --git a/frontend/public/Icons/groupBy.svg b/frontend/public/Icons/groupBy.svg new file mode 100644 index 0000000000..e668ef176a --- /dev/null +++ b/frontend/public/Icons/groupBy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/CustomIcons/GroupByIcon.tsx b/frontend/src/assets/CustomIcons/GroupByIcon.tsx new file mode 100644 index 0000000000..4cfceef3c8 --- /dev/null +++ b/frontend/src/assets/CustomIcons/GroupByIcon.tsx @@ -0,0 +1,27 @@ +import { Color } from '@signozhq/design-tokens'; +import { useIsDarkMode } from 'hooks/useDarkMode'; + +function GroupByIcon(): JSX.Element { + const isDarkMode = useIsDarkMode(); + return ( + + + + + + + + + + + + ); +} + +export default GroupByIcon; diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2a2dd56855..2c56d58fd1 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { VIEWS } from './constants'; export type LogDetailProps = { log: ILog | null; selectedTab: VIEWS; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; } & Pick & diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 650d474736..5421672529 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -37,6 +37,7 @@ function LogDetail({ log, onClose, onAddToQuery, + onGroupByAttribute, onClickActionItem, selectedTab, isListViewPanel = false, @@ -209,6 +210,7 @@ function LogDetail({ logData={log} onAddToQuery={onAddToQuery} onClickActionItem={onClickActionItem} + onGroupByAttribute={onGroupByAttribute} isListViewPanel={isListViewPanel} selectedOptions={options} listViewPanelSelectedFields={listViewPanelSelectedFields} diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index d1461b0caa..34b3fddd19 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -141,6 +141,7 @@ function ListLogView({ onAddToQuery: handleAddToQuery, onSetActiveLog: handleSetActiveContextLog, onClearActiveLog: handleClearActiveContextLog, + onGroupByAttribute, } = useActiveLog(); const isDarkMode = useIsDarkMode(); @@ -257,6 +258,7 @@ function ListLogView({ onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} onClose={handlerClearActiveContextLog} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index b4b3eb7783..935f423393 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -55,6 +55,7 @@ function RawLogView({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const [hasActionButtons, setHasActionButtons] = useState(false); @@ -202,6 +203,7 @@ function RawLogView({ onClose={handleCloseLogDetail} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx index 50beda2953..0be9334849 100644 --- a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx @@ -38,6 +38,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -151,6 +152,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { log={activeLog} onClose={onClearActiveLog} onAddToQuery={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/LogDetailedView/Overview.tsx b/frontend/src/container/LogDetailedView/Overview.tsx index 9054217c33..1abfa5a526 100644 --- a/frontend/src/container/LogDetailedView/Overview.tsx +++ b/frontend/src/container/LogDetailedView/Overview.tsx @@ -18,6 +18,7 @@ import { ChevronDown, ChevronRight, Search } from 'lucide-react'; import { ReactNode, useState } from 'react'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import TableView from './TableView'; @@ -27,6 +28,11 @@ interface OverviewProps { isListViewPanel?: boolean; selectedOptions: OptionsQuery; listViewPanelSelectedFields?: IField[] | null; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; } type Props = OverviewProps & @@ -39,6 +45,7 @@ function Overview({ onClickActionItem, isListViewPanel = false, selectedOptions, + onGroupByAttribute, listViewPanelSelectedFields, }: Props): JSX.Element { const [isWrapWord, setIsWrapWord] = useState(true); @@ -204,6 +211,7 @@ function Overview({ logData={logData} onAddToQuery={onAddToQuery} fieldSearchInput={fieldSearchInput} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onClickActionItem} isListViewPanel={isListViewPanel} selectedOptions={selectedOptions} @@ -222,6 +230,7 @@ function Overview({ Overview.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; export default Overview; diff --git a/frontend/src/container/LogDetailedView/TableView.styles.scss b/frontend/src/container/LogDetailedView/TableView.styles.scss index 2f092dc04d..322a5cd638 100644 --- a/frontend/src/container/LogDetailedView/TableView.styles.scss +++ b/frontend/src/container/LogDetailedView/TableView.styles.scss @@ -11,7 +11,7 @@ top: 50%; right: 16px; transform: translateY(-50%); - gap: 8px; + gap: 4px; } } } @@ -76,8 +76,10 @@ box-shadow: none; border-radius: 2px; background: var(--bg-slate-400); - - height: 24px; + padding: 2px 3px; + gap: 3px; + height: 18px; + width: 20px; } } } diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 0508701af5..591109ac3c 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -4,13 +4,12 @@ import './TableView.styles.scss'; import { LinkOutlined } from '@ant-design/icons'; 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 cx from 'classnames'; import AddToQueryHOC, { AddToQueryHOCProps, } from 'components/Logs/AddToQueryHOC'; -import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import { ResizeTable } from 'components/ResizeTable'; import { OPERATORS } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; @@ -19,8 +18,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode'; import history from 'lib/history'; import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; -import { isEmpty } from 'lodash-es'; -import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react'; +import { Pin } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; 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 { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; -import { - filterKeyForField, - findKeyPath, - flattenObject, - jsonToDataNodes, - recursiveParseJSON, - removeEscapeCharacters, -} from './utils'; +import { TableViewActions } from './TableView/TableViewActions'; +import { filterKeyForField, findKeyPath, flattenObject } from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -50,6 +43,11 @@ interface TableViewProps { selectedOptions: OptionsQuery; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; } type Props = TableViewProps & @@ -63,6 +61,7 @@ function TableView({ onClickActionItem, isListViewPanel = false, selectedOptions, + onGroupByAttribute, listViewPanelSelectedFields, }: Props): JSX.Element | null { const dispatch = useDispatch>(); @@ -271,75 +270,17 @@ function TableView({ width: 70, ellipsis: false, className: 'value-field-container attribute-value', - render: (fieldData: Record, record): JSX.Element => { - const textToCopy = fieldData.value.slice(1, -1); - - if (record.field === 'body') { - const parsedBody = recursiveParseJSON(fieldData.value); - if (!isEmpty(parsedBody)) { - return ( - - ); - } - } - - const fieldFilterKey = filterKeyForField(fieldData.field); - - return ( -
- - - {removeEscapeCharacters(fieldData.value)} - - - - {!isListViewPanel && ( - - -
- ); - }, + render: (fieldData: Record, record): JSX.Element => ( + + ), }, ]; function sortPinnedAttributes( @@ -380,9 +321,10 @@ function TableView({ TableView.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; -interface DataType { +export interface DataType { key: string; field: string; value: string; diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss new file mode 100644 index 0000000000..f5a45ef416 --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss @@ -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); + } + } + } +} diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx new file mode 100644 index 0000000000..b239e64d3a --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -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; + record: DataType; + isListViewPanel: boolean; + isfilterInLoading: boolean; + isfilterOutLoading: boolean; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; + 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(false); + const textToCopy = fieldData.value.slice(1, -1); + + if (record.field === 'body') { + const parsedBody = recursiveParseJSON(fieldData.value); + if (!isEmpty(parsedBody)) { + return ( + + ); + } + } + + const fieldFilterKey = filterKeyForField(fieldData.field); + + return ( +
+ + + {removeEscapeCharacters(fieldData.value)} + + + + {!isListViewPanel && ( + + + +
+ } + rootClassName="table-view-actions-content" + trigger="hover" + placement="bottomLeft" + > +