From 085e93710f3ab64cfad48dadefaaf3e1110ed503 Mon Sep 17 00:00:00 2001 From: vitshev Date: Mon, 19 Feb 2024 17:36:20 +0100 Subject: [PATCH] feat(QueryTracker): new columns to the list of queries [#267] --- .../ui/src/shared/constants/settings-types.ts | 2 + .../src/ui/components/common/Timeline/util.ts | 4 + .../QueriesList/QueriesHistoryList/index.scss | 18 ++- .../QueriesList/QueriesHistoryList/index.tsx | 147 ++++++++++++++---- .../QueriesHistoryList/useQueryListColumns.ts | 71 +++++++++ .../QueriesList/QueriesListFilter/index.scss | 4 + .../QueriesList/QueriesListFilter/index.tsx | 19 +++ .../module/queries_list/selectors.ts | 9 ++ 8 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/useQueryListColumns.ts diff --git a/packages/ui/src/shared/constants/settings-types.ts b/packages/ui/src/shared/constants/settings-types.ts index ba5b3ec12..d5b34521c 100644 --- a/packages/ui/src/shared/constants/settings-types.ts +++ b/packages/ui/src/shared/constants/settings-types.ts @@ -70,6 +70,8 @@ interface AccountsSettings { interface QueryTrackerSettings { 'global::queryTracker::queriesListSidebarVisibilityMode': boolean; + 'local::queryTracker::history::AllColumns': string[]; + 'local::queryTracker::history::MyColumns': string[]; } interface ChytSettings { 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..87c3fb941 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,27 @@ &__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; + } } } @@ -76,4 +88,8 @@ align-items: center; padding: 8px 0; } + + &__row-header { + padding: 6px 0; + } } 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..5f02fa959 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 {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,26 @@ function useQueryHistoryList() { return useQueryList(); } -const NameColumns: Column = { +type HeaderTableItem = {header: string}; +type TableItem = QueryItem | HeaderTableItem; + +const isQueryItem = (b: TableItem): b is QueryItem => { + return (b as QueryItem).id !== undefined; +}; + +const isHeaderTableItem = (b: TableItem): b is HeaderTableItem => { + return (b as HeaderTableItem).header !== undefined; +}; + +const NameColumns: Column = { name: 'Name', align: 'left', className: itemBlock('name_row'), render: ({row}) => { + if (isHeaderTableItem(row)) { + return null; + } + const name = row.annotations?.title; return (
@@ -103,11 +121,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 +138,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 +154,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 = { +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 +189,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 +232,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 +246,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,20 +260,48 @@ export function QueriesHistoryList() { [selectedId], ); + const itemsByDate = 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); + return (
- className={b('list')} loading={isLoading} columns={columns} - data={items} + data={itemsByDate} useThemeYT={true} - rowKey={(row) => row.id} - onRowClick={goToQuery} + rowKey={(row) => (row as QueryItem).id} + onRowClick={(item) => goToQuery(item as QueryItem)} disableRightGap={true} settings={tableSettings} rowClassName={setClassName} + renderRow={({defaultRender, columns, index, className}) => { + const item = itemsByDate[index]; + if (isQueryItem(item)) { + return defaultRender(); + } + + if (isHeaderTableItem(item)) { + return ( + + +

{item.header}

+ + + ); + } + + return null; + }} />
{(!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..7ca7a5487 --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueriesList/QueriesHistoryList/useQueryListColumns.ts @@ -0,0 +1,71 @@ +import {useMemo} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {QueriesListAuthorFilter} from '../../module/queries_list/types'; +import {AllColumns, MyColumns} from './index'; +import {setSettingByKey} from '../../../../store/actions/settings'; +import { + getQueryListHistoryAllColumns, + getQueryListHistoryMyColumns, +} from '../../module/queries_list/selectors'; + +export const useQueryHistoryListColumns = ({type}: {type?: QueriesListAuthorFilter}) => { + const dispatch = useDispatch(); + const selectedMyColumns = useSelector(getQueryListHistoryMyColumns); + const selectedAllColumns = useSelector(getQueryListHistoryAllColumns); + const settingKey = + type === QueriesListAuthorFilter.My + ? 'local::queryTracker::history::MyColumns' + : 'local::queryTracker::history::AllColumns'; + + const handleColumnChange = (selectedColumns: { + items: Array<{checked: boolean; name: string}>; + }) => { + dispatch( + setSettingByKey( + settingKey, + selectedColumns.items.filter(({checked}) => checked).map(({name}) => name), + ), + ); + }; + + const {columns, allowedColumns} = useMemo(() => { + switch (type) { + case QueriesListAuthorFilter.My: { + const cols = selectedMyColumns.length + ? selectedMyColumns + : MyColumns.map((item) => item.name); + + return { + columns: MyColumns.filter(({name}) => cols.includes(name)), + allowedColumns: MyColumns.map(({name}) => ({ + name, + checked: cols.includes(name), + })), + }; + } + + case QueriesListAuthorFilter.All: { + const cols = selectedAllColumns.length + ? selectedAllColumns + : AllColumns.map((item) => item.name); + + return { + columns: AllColumns.filter(({name}) => cols.includes(name)), + allowedColumns: AllColumns.map(({name}) => ({ + name, + checked: cols.includes(name), + })), + }; + } + + default: + return {columns: [], allowedColumns: []}; + } + }, [type, selectedAllColumns, selectedMyColumns]); + + return { + columns, + allowedColumns, + 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..df1d42462 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 @@ -4,6 +4,7 @@ import {getCurrentUserName} 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,11 @@ export function getQueriesListFilterParams(state: RootState): QueriesListParams user: undefined, }; } + +export function getQueryListHistoryMyColumns(state: RootState) { + return getSettingsData(state)['local::queryTracker::history::MyColumns'] ?? []; +} + +export function getQueryListHistoryAllColumns(state: RootState) { + return getSettingsData(state)['local::queryTracker::history::AllColumns'] ?? []; +}