diff --git a/packages/ui/package-lock.json b/packages/ui/package-lock.json index a6b6cba6d..e44205796 100644 --- a/packages/ui/package-lock.json +++ b/packages/ui/package-lock.json @@ -42,7 +42,7 @@ "@gravity-ui/i18n": "^1.0.0", "@gravity-ui/icons": "^2.4.0", "@gravity-ui/prettier-config": "^1.1.0", - "@gravity-ui/react-data-table": "^1.2.0", + "@gravity-ui/react-data-table": "^1.3.0", "@gravity-ui/stylelint-config": "^3.0.0", "@gravity-ui/tsconfig": "^1.0.0", "@gravity-ui/uikit": "^5.16.0", @@ -4683,11 +4683,10 @@ } }, "node_modules/@gravity-ui/react-data-table": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/react-data-table/-/react-data-table-1.2.0.tgz", - "integrity": "sha512-+FzUT7KlNy3MrakMxRHWSRjhph01EkH6Oil4pFptdp2QNXBZlyr8Y5fMu6pzAybbPs/E/pFbrpBo3oxT1QGWYw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/react-data-table/-/react-data-table-1.3.0.tgz", + "integrity": "sha512-ZWzMOMQtpA/pj4MaIG4hTsUOk0JcCcfuX5xu+qYCWx4zMJ7ug5PLGr14DwX4j3cBI0PjkObNRQPrYvwEujuNlg==", "dev": true, - "license": "MIT", "dependencies": { "react-list": "0.8.16" }, diff --git a/packages/ui/package.json b/packages/ui/package.json index 00c903730..efe9ba61f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -91,7 +91,7 @@ "@gravity-ui/i18n": "^1.0.0", "@gravity-ui/icons": "^2.4.0", "@gravity-ui/prettier-config": "^1.1.0", - "@gravity-ui/react-data-table": "^1.2.0", + "@gravity-ui/react-data-table": "^1.3.0", "@gravity-ui/stylelint-config": "^3.0.0", "@gravity-ui/tsconfig": "^1.0.0", "@gravity-ui/uikit": "^5.16.0", diff --git a/packages/ui/src/shared/constants/settings-types.ts b/packages/ui/src/shared/constants/settings-types.ts index ba5b3ec12..effbd1709 100644 --- a/packages/ui/src/shared/constants/settings-types.ts +++ b/packages/ui/src/shared/constants/settings-types.ts @@ -68,16 +68,20 @@ interface AccountsSettings { 'global::accounts::dashboardVisibilityMode': 'string'; } +type ClusterName = string; + interface QueryTrackerSettings { 'global::queryTracker::queriesListSidebarVisibilityMode': boolean; } +type QueryTrackerHistoryColumnsSettings = { + [key in `local::${ClusterName}::queryTracker::history::Columns`]: string[]; +}; + interface ChytSettings { 'global::chyt::list_columns': Array; } -type ClusterName = string; - type QueryTrackerLastSelectedACOsSettings = { [key in `local::${ClusterName}::queryTracker::lastSelectedACOs`]: string[]; }; @@ -96,7 +100,7 @@ export interface DefaultSettings { A11Y: A11YSettings; MENU: MenuSettings; ACCOUNTS: AccountsSettings; - QUERY_TRACKER: QueryTrackerSettings; + QUERY_TRACKER: QueryTrackerSettings | QueryTrackerHistoryColumnsSettings; CHYT: ChytSettings; } @@ -111,7 +115,8 @@ type DescribedSettings = GlobalSettings & AccountsSettings & QueryTrackerSettings & ChytSettings & - QueryTrackerLastSelectedACOsSettings; + QueryTrackerLastSelectedACOsSettings & + QueryTrackerHistoryColumnsSettings; export type Settings = DescribedSettings & OtherSettings; diff --git a/packages/ui/src/ui/components/common/Timeline/util.ts b/packages/ui/src/ui/components/common/Timeline/util.ts index caca23d92..bd69cb7d5 100644 --- a/packages/ui/src/ui/components/common/Timeline/util.ts +++ b/packages/ui/src/ui/components/common/Timeline/util.ts @@ -60,6 +60,10 @@ export const formatInterval = (from: MomentInput, to: MomentInput) => { } }; +export const formatTime = (date: MomentInput) => { + return moment(date).format('HH:mm'); +}; + export const formatDateCompact = (date: MomentInput) => { return moment(date).format('DD.MM.YY, HH:mm'); }; diff --git a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.scss b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.scss index cfbc19539..2c26d707e 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.scss +++ b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.scss @@ -32,15 +32,31 @@ &__name-edit { visibility: hidden; - width: 0; + flex: 1px; + overflow: hidden; z-index: 1; margin: -14px 0; } &:hover &__name-edit { + flex: 1; padding-left: 5px; visibility: visible; width: auto; + overflow: visible; + } + + &_header { + padding: 8px 0; + + &:hover { + background: transparent; + cursor: auto; + } + } + + &__separator { + text-align: center; } } diff --git a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.tsx b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.tsx index 6f16876e2..1dd6950d1 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/index.tsx @@ -1,28 +1,31 @@ -import hammer from '../../../../common/hammer'; +import moment from 'moment'; +import groupBy from 'lodash/groupBy'; +import noop from 'lodash/noop'; import {Text} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; -import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {useDispatch, useSelector} from 'react-redux'; +import DataTable, {Column, Settings} from '@gravity-ui/react-data-table'; import {QueryItem, QueryStatus} from '../../module/api'; import {refreshQueriesListIfNeeded} from '../../module/queries_list/actions'; import {getQueriesListTimestamp, getUncompletedItems} from '../../module/queries_list/selectors'; import {QueryStatusIcon} from '../../QueryStatus'; +import hammer from '../../../../common/hammer'; -import './index.scss'; import Pagination from '../../../../components/Pagination/Pagination'; -import {noop} from 'lodash'; import {QueriesPoolingContext} from '../../hooks/QueriesPooling/context'; -import {formatDateCompact} from '../../../../components/common/Timeline/util'; +import {formatTime} from '../../../../components/common/Timeline/util'; import {QueryEnginesNames} from '../../utils/query'; import DataTableYT from '../../../../components/DataTableYT/DataTableYT'; -import DataTable, {Column, Settings} from '@gravity-ui/react-data-table'; import {useQuriesHistoryFilter} from '../../hooks/QueryListFilter'; -import {QueriesListAuthorFilter} from '../../module/queries_list/types'; import {QueryDuration} from '../../QueryDuration'; import {useQueryNavigation} from '../../hooks/Query'; import {useQueriesPagination, useQueryList} from '../../hooks/QueriesList'; import EditQueryNameModal from '../EditQueryNameModal/EditQueryNameModal'; import {UPDATE_QUERIES_LIST} from '../../module/query-tracker-contants'; +import {useQueryHistoryListColumns} from './useQueryListColumns'; + +import './index.scss'; const b = block('queries-history-list'); @@ -81,11 +84,22 @@ function useQueryHistoryList() { return useQueryList(); } -const NameColumns: Column = { +type HeaderTableItem = {header: string}; +type TableItem = QueryItem | HeaderTableItem; + +const isHeaderTableItem = (b: TableItem): b is HeaderTableItem => { + return (b as HeaderTableItem).header !== undefined; +}; + +export const NameColumns: Column = { name: 'Name', align: 'left', className: itemBlock('name_row'), render: ({row}) => { + if (isHeaderTableItem(row)) { + return
{row.header}
; + } + const name = row.annotations?.title; return (
@@ -103,11 +117,15 @@ const NameColumns: Column = { }, }; -const TypeColumns: Column = { +const TypeColumns: Column = { name: 'Type', align: 'center', width: 60, render: ({row}) => { + if (isHeaderTableItem(row)) { + return null; + } + return ( {row.engine in QueryEnginesNames ? QueryEnginesNames[row.engine] : row.engine} @@ -116,11 +134,15 @@ const TypeColumns: Column = { }, }; -const DurationColumns: Column = { +const DurationColumns: Column = { name: 'Duration', align: 'left', width: 100, render: ({row}) => { + if (isHeaderTableItem(row)) { + return null; + } + if (row.state === QueryStatus.RUNNING) { return hammer.format.NO_VALUE; } @@ -128,25 +150,33 @@ const DurationColumns: Column = { }, }; -const StartedColumns: Column = { +const StartedColumns: Column = { name: 'Started', align: 'left', - width: 120, + width: 60, render: ({row}) => { + if (isHeaderTableItem(row)) { + return null; + } + return ( - {formatDateCompact(row.start_time)} + {formatTime(row.start_time)} ); }, }; -const AuthorColumns: Column = { +export const AuthorColumns: Column = { name: 'Author', align: 'left', width: 120, className: itemBlock('author_row'), render: ({row}) => { + if (isHeaderTableItem(row)) { + return null; + } + return ( {row.user} @@ -155,13 +185,38 @@ const AuthorColumns: Column = { }, }; -const MyColumns: Column[] = [NameColumns, TypeColumns, DurationColumns, StartedColumns]; -const AllColumns: Column[] = [ +const ACOColumns: Column = { + name: 'ACO', + align: 'left', + width: 60, + className: itemBlock('access_control_object'), + render: ({row}) => { + if (isHeaderTableItem(row)) { + return null; + } + + return ( + + {row.access_control_object} + + ); + }, +}; + +export const MyColumns: Column[] = [ + NameColumns, + TypeColumns, + DurationColumns, + StartedColumns, + ACOColumns, +]; +export const AllColumns: Column[] = [ NameColumns, TypeColumns, DurationColumns, AuthorColumns, StartedColumns, + ACOColumns, ]; const tableSettings: Settings = { @@ -173,25 +228,13 @@ const tableSettings: Settings = { export function QueriesHistoryList() { const [items, isLoading] = useQueryHistoryList(); - const [filter] = useQuriesHistoryFilter(); - + const {columns} = useQueryHistoryListColumns({type: filter.user}); const timestamp = useSelector(getQueriesListTimestamp); - const {first, last, goBack, goNext, goFirst} = useQueriesPagination(); - const [selectedId, goToQuery] = useQueryNavigation(); - - const [columns, setColumns] = useState[]>([]); - const scrollElemRef = useRef(null); - useEffect(() => { - if (!isLoading || !items?.length) { - setColumns(filter.user === QueriesListAuthorFilter.My ? MyColumns : AllColumns); - } - }, [items, setColumns, filter.user, isLoading]); - useEffect(() => { if (scrollElemRef?.current) { scrollElemRef.current.scrollTop = 0; @@ -199,7 +242,13 @@ export function QueriesHistoryList() { }, [scrollElemRef, timestamp]); const setClassName = useCallback( - (item: QueryItem) => { + (item: TableItem) => { + if (isHeaderTableItem(item)) { + return itemBlock({ + header: Boolean(item.header), + }); + } + return itemBlock({ selected: item.id === selectedId, }); @@ -207,6 +256,20 @@ export function QueriesHistoryList() { [selectedId], ); + const itemsByDate = useMemo( + () => + Object.entries( + groupBy(items, (item) => moment(item.start_time).format('DD MMMM YYYY')), + ).reduce((ret, [header, items]) => { + ret.push({ + header, + }); + + return ret.concat(items.map((item) => item)); + }, [] as Array), + [items], + ); + return (
@@ -214,13 +277,32 @@ export function QueriesHistoryList() { className={b('list')} loading={isLoading} columns={columns} - data={items} + data={itemsByDate} useThemeYT={true} - rowKey={(row) => row.id} - onRowClick={goToQuery} + rowKey={(row) => { + if (isHeaderTableItem(row)) { + return row.header; + } + + return row.id; + }} + onRowClick={(item) => { + if (!isHeaderTableItem(item)) { + goToQuery(item); + } + }} disableRightGap={true} settings={tableSettings} rowClassName={setClassName} + getColSpansOfRow={({row}) => { + if (isHeaderTableItem(row)) { + return { + Name: columns.length, + }; + } + + return undefined; + }} />
{(!first || !last) && ( diff --git a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/useQueryListColumns.ts b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/useQueryListColumns.ts new file mode 100644 index 000000000..883478620 --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/useQueryListColumns.ts @@ -0,0 +1,42 @@ +import {useDispatch, useSelector} from 'react-redux'; +import intersectionBy from 'lodash/intersectionBy'; +import {QueriesListAuthorFilter} from '../../module/queries_list/types'; +import {AllColumns, AuthorColumns, MyColumns, NameColumns} from './index'; +import {setSettingByKey} from '../../../../store/actions/settings'; +import {getQueryListHistoryColumns} from '../../module/queries_list/selectors'; +import {getSettingsCluster} from '../../../../store/selectors/global'; + +export const useQueryHistoryListColumns = ({type}: {type?: QueriesListAuthorFilter}) => { + const ALL_COLUMN_NAMES = intersectionBy(AllColumns, MyColumns, 'name').map((item) => item.name); + const EXCLUDED_COLUMNS = [NameColumns.name, AuthorColumns.name]; + const dispatch = useDispatch(); + const selectedColumns = useSelector(getQueryListHistoryColumns); + const settingsCluster = useSelector(getSettingsCluster); + const currentColumnsPreset = type === QueriesListAuthorFilter.My ? MyColumns : AllColumns; + + const handleColumnChange = (selectedColumns: { + items: Array<{checked: boolean; name: string}>; + }) => { + dispatch( + setSettingByKey( + `local::${settingsCluster}::queryTracker::history::Columns`, + selectedColumns.items.filter(({checked}) => checked).map(({name}) => name), + ), + ); + }; + + const selectedColumnNames = new Set( + Array.isArray(selectedColumns) ? selectedColumns : ALL_COLUMN_NAMES, + ); + + selectedColumnNames.add(NameColumns.name); + selectedColumnNames.add(AuthorColumns.name); + + return { + columns: currentColumnsPreset.filter(({name}) => selectedColumnNames.has(name)), + allowedColumns: currentColumnsPreset + .filter((item) => !EXCLUDED_COLUMNS.includes(item.name)) + .map(({name}) => ({name, checked: selectedColumnNames.has(name)})), + handleColumnChange, + }; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.scss b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.scss index 6b1402a3f..361d828f5 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.scss +++ b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.scss @@ -17,4 +17,8 @@ &__row-item:not(:first-child) { margin-left: 8px; } + + &__columns-button { + margin-left: 8px; + } } diff --git a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.tsx b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.tsx index f861b27b9..66c1c3e1a 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesListFilter/index.tsx @@ -8,6 +8,11 @@ import {QueryEngineFilter} from './QueryEngineFilter'; import {QueryEngine} from '../../module/engines'; import {QueryTextFilter} from './QueryTextFilter'; import {useQuriesHistoryFilter} from '../../hooks/QueryListFilter'; +import Dropdown from '../../../../components/Dropdown/Dropdown'; +import Button from '../../../../components/Button/Button'; +import Icon from '../../../../components/Icon/Icon'; +import ColumnSelector from '../../../../components/ColumnSelector/ColumnSelector'; +import {useQueryHistoryListColumns} from '../QueriesHistoryList/useQueryListColumns'; const AuthorFilter: ControlGroupOption[] = [ { @@ -25,8 +30,10 @@ const b = block('queries-history-filter'); type QueriesHistoryListFilterProps = { className?: string; }; + export function QueriesHistoryListFilter({className}: QueriesHistoryListFilterProps) { const [filter, filterViewMode, onChange] = useQuriesHistoryFilter(); + const {allowedColumns, handleColumnChange} = useQueryHistoryListColumns({type: filter.user}); const onChangeAuthorFilter = useCallback( (user: string) => { @@ -70,6 +77,18 @@ export function QueriesHistoryListFilter({className}: QueriesHistoryListFilterPr value={filter?.filter} onChange={onChangeTextFilter} /> + + + + } + template={ + + } + />
)}
diff --git a/packages/ui/src/ui/pages/query-tracker/module/queries_list/selectors.ts b/packages/ui/src/ui/pages/query-tracker/module/queries_list/selectors.ts index b0a2783f3..48bfaa9f9 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/queries_list/selectors.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/queries_list/selectors.ts @@ -1,9 +1,10 @@ import {createSelector} from 'reselect'; import {RootState} from '../../../../store/reducers'; -import {getCurrentUserName} from '../../../../store/selectors/global'; +import {getCurrentUserName, getSettingsCluster} from '../../../../store/selectors/global'; import {QueriesListParams} from '../api'; import {isQueryProgress} from '../../utils/query'; import {QueriesListFilterPresets} from './types'; +import {getSettingsData} from '../../../../store/selectors/settings-base'; export const getQueriesListState = (state: RootState) => state.queryTracker.list; @@ -75,3 +76,9 @@ export function getQueriesListFilterParams(state: RootState): QueriesListParams user: undefined, }; } + +export function getQueryListHistoryColumns(state: RootState) { + const cluster = getSettingsCluster(state); + + return getSettingsData(state)[`local::${cluster}::queryTracker::history::Columns`]; +}