diff --git a/package-lock.json b/package-lock.json index 48437e1cd..b34b87455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "@gravity-ui/tsconfig": "^1.0.0", "@playwright/test": "^1.42.1", "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^14.2.2", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", @@ -5732,22 +5732,23 @@ } }, "node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", + "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@testing-library/dom/node_modules/chalk": { @@ -5755,6 +5756,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5771,6 +5773,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5785,6 +5788,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -5837,15 +5841,6 @@ } } }, - "node_modules/@testing-library/jest-dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -5853,21 +5848,30 @@ "dev": true }, "node_modules/@testing-library/react": { - "version": "14.2.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.2.tgz", - "integrity": "sha512-SOUuM2ysCvjUWBXTNfQ/ztmnKDmqaiPV3SvoIuyxMUca45rbSWWAT/qB8CUs/JQ/ux/8JFs9DNdFQ3f6jH3crA==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", + "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@testing-library/user-event": { @@ -5905,7 +5909,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.0", @@ -7143,12 +7148,12 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -9950,40 +9955,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "node_modules/deep-equal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", - "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-equal/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10263,7 +10234,8 @@ "version": "0.5.12", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.12.tgz", "integrity": "sha512-gQ2mON6fLWZeM8ubjzL7RtMeHS/g8hb82j4MjHmcQECD7pevWsMlhqwp9BjIRrQvmyJMMyv/XiO1cXzeFlUw4g==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -10611,32 +10583,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/es-iterator-helpers": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", @@ -11074,15 +11020,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -13426,22 +13363,6 @@ "node": ">= 10" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -17533,6 +17454,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -18398,22 +18320,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -24019,18 +23925,6 @@ "node": ">= 0.8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/package.json b/package.json index e5fdbe555..1ab61cd9d 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "@gravity-ui/tsconfig": "^1.0.0", "@playwright/test": "^1.42.1", "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^14.2.2", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", diff --git a/src/containers/Storage/Storage.tsx b/src/containers/Storage/Storage.tsx index 3d2014bb6..e2483beac 100644 --- a/src/containers/Storage/Storage.tsx +++ b/src/containers/Storage/Storage.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import type {OrderType, SortOrder} from '@gravity-ui/react-data-table'; + import {AccessDenied} from '../../components/Errors/403'; import {isAccessError} from '../../components/Errors/PageError/PageError'; import {ResponseError} from '../../components/Errors/ResponseError'; @@ -13,6 +15,7 @@ import type {NodesSortParams} from '../../store/reducers/nodes/types'; import {filterGroups, filterNodes} from '../../store/reducers/storage/selectors'; import {storageApi} from '../../store/reducers/storage/storage'; import type {StorageSortParams} from '../../store/reducers/storage/types'; +import type {HandleSort} from '../../utils/hooks'; import {useAutoRefreshInterval, useTableSort} from '../../utils/hooks'; import {useAdditionalNodeProps} from '../AppWithClusters/useClusterData'; @@ -28,6 +31,32 @@ import {defaultSortNode, getDefaultSortGroup} from './utils'; import './Storage.scss'; +export function useStorageSort< + T extends { + sortValue: string; + sortOrder: OrderType; + }, +>( + {sortValue, sortOrder}: T, + onSort: (params: T | undefined) => void, +): [SortOrder[] | undefined, HandleSort] { + return useTableSort({ + initialSortColumn: sortValue, + initialSortOrder: sortOrder, + multiple: false, + onSort: (sort) => { + const newSort = sort?.[0] + ? { + sortValue: sort[0].columnId, + sortOrder: sort[0].order, + } + : undefined; + + onSort(newSort as T); + }, + }); +} + interface StorageProps { database?: string; nodeId?: string | number; @@ -58,17 +87,11 @@ export const Storage = ({database, viewContext, nodeId, groupId, pDiskId}: Stora const isGroups = storageType === 'groups'; const isNodes = storageType === 'nodes'; - const [nodeSort, setNodeSort] = React.useState({ - sortOrder: undefined, - sortValue: undefined, - }); - const nodesSortParams = nodeSort.sortValue ? nodeSort : defaultSortNode; + const [nodeSort, setNodeSort] = React.useState(undefined); + const nodesSortParams = nodeSort ? nodeSort : defaultSortNode; - const [groupSort, setGroupSort] = React.useState({ - sortOrder: undefined, - sortValue: undefined, - }); - const groupsSortParams = groupSort.sortOrder ? groupSort : getDefaultSortGroup(visibleEntities); + const [groupSort, setGroupSort] = React.useState(undefined); + const groupsSortParams = groupSort ? groupSort : getDefaultSortGroup(visibleEntities); const { columnsToShow: storageNodesColumnsToShow, @@ -134,12 +157,8 @@ export const Storage = ({database, viewContext, nodeId, groupId, pDiskId}: Stora [searchValue, groups], ); - const [nodesSort, handleNodesSort] = useTableSort(nodesSortParams, (params) => - setNodeSort(params as NodesSortParams), - ); - const [groupsSort, handleGroupsSort] = useTableSort(groupsSortParams, (params) => - setGroupSort(params as StorageSortParams), - ); + const [nodesSort, handleNodesSort] = useStorageSort(nodesSortParams, setNodeSort); + const [groupsSort, handleGroupsSort] = useStorageSort(groupsSortParams, setGroupSort); const renderDataTable = () => { return ( diff --git a/src/containers/Storage/StorageGroups/StorageGroupsTable.tsx b/src/containers/Storage/StorageGroups/StorageGroupsTable.tsx index d415fbaed..84b911ddc 100644 --- a/src/containers/Storage/StorageGroups/StorageGroupsTable.tsx +++ b/src/containers/Storage/StorageGroups/StorageGroupsTable.tsx @@ -16,7 +16,7 @@ interface StorageGroupsTableProps { columns: StorageGroupsColumn[]; visibleEntities: VisibleEntities; onShowAll?: VoidFunction; - sort?: SortOrder; + sort?: SortOrder[]; handleSort?: HandleSort; } diff --git a/src/containers/Storage/StorageNodes/StorageNodesTable.tsx b/src/containers/Storage/StorageNodes/StorageNodesTable.tsx index 667d450b6..2d99a0d2a 100644 --- a/src/containers/Storage/StorageNodes/StorageNodesTable.tsx +++ b/src/containers/Storage/StorageNodes/StorageNodesTable.tsx @@ -23,7 +23,7 @@ interface StorageNodesTableProps { columns: StorageNodesColumn[]; visibleEntities: VisibleEntities; nodesUptimeFilter: NodesUptimeFilterValues; - sort?: SortOrder; + sort?: SortOrder[]; onShowAll?: VoidFunction; handleSort?: HandleSort; } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index 0761e8a21..a3bc578b6 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -18,10 +18,8 @@ import {useAutoRefreshInterval, useTypedDispatch} from '../../../../../utils/hoo import {useChangeInputWithConfirmation} from '../../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; import {parseQueryErrorToString} from '../../../../../utils/query'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; -import { - TENANT_OVERVIEW_TOP_QUERUES_COLUMNS, - TOP_QUERIES_COLUMNS_WIDTH_LS_KEY, -} from '../../TopQueries/getTopQueriesColumns'; +import {getTenantOverviewTopQueriesColumns} from '../../TopQueries/columns/columns'; +import {TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} from '../../TopQueries/columns/constants'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; import {getSectionTitle} from '../getSectionTitle'; import i18n from '../i18n'; @@ -39,7 +37,10 @@ export function TopQueries({tenantName}: TopQueriesProps) { const query = parseQuery(location); const [autoRefreshInterval] = useAutoRefreshInterval(); - const columns = TENANT_OVERVIEW_TOP_QUERUES_COLUMNS; + + const columns = React.useMemo(() => { + return getTenantOverviewTopQueriesColumns(); + }, []); const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( {database: tenantName}, diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx index c1e4a0c78..af8ac4f26 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx @@ -6,10 +6,8 @@ import {topShardsApi} from '../../../../../store/reducers/tenantOverview/topShar import {useAutoRefreshInterval} from '../../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../../utils/query'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; -import { - TOP_SHARDS_COLUMNS_WIDTH_LS_KEY, - getTopShardsColumns, -} from '../../TopShards/getTopShardsColumns'; +import {getTopShardsColumns} from '../../TopShards/columns/columns'; +import {TOP_SHARDS_COLUMNS_WIDTH_LS_KEY} from '../../TopShards/columns/constants'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; import {getSectionTitle} from '../getSectionTitle'; import i18n from '../i18n'; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index a55ffeb1a..1d0e011c9 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -7,13 +7,11 @@ import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/execut import type {KeyValueRow} from '../../../../types/api/query'; import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../utils/query'; -import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; -import { - RUNNING_QUERIES_COLUMNS, - RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY, -} from './getTopQueriesColumns'; +import {getRunningQueriesColumns} from './columns/columns'; +import {RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY} from './columns/constants'; import i18n from './i18n'; +import {TOP_QUERIES_TABLE_SETTINGS, useRunningQueriesSort} from './utils'; interface Props { database: string; @@ -24,16 +22,26 @@ interface Props { export const RunningQueriesData = ({database, onRowClick, rowClassName}: Props) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); - const {currentData, isLoading, error} = topQueriesApi.useGetRunningQueriesQuery( + + const {tableSort, handleTableSort, backendSort} = useRunningQueriesSort(); + + const {currentData, isFetching, error} = topQueriesApi.useGetRunningQueriesQuery( { database, filters, + sortOrder: backendSort, }, {pollingInterval: autoRefreshInterval}, ); + const loading = isFetching && currentData === undefined; + const data = currentData?.resultSets?.[0].result || []; + const columns = React.useMemo(() => { + return getRunningQueriesColumns(); + }, []); + const handleRowClick = (row: KeyValueRow) => { return onRowClick(row.QueryText as string); }; @@ -41,15 +49,17 @@ export const RunningQueriesData = ({database, onRowClick, rowClassName}: Props) return ( {error ? : null} - + rowClassName} + sortOrder={tableSort} + onSort={handleTableSort} /> diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index 10c32046e..d447fbef8 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -5,13 +5,13 @@ import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/Re import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; import type {KeyValueRow} from '../../../../types/api/query'; -import {isSortableTopQueriesProperty} from '../../../../utils/diagnostics'; import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../utils/query'; -import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; -import {TOP_QUERIES_COLUMNS, TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} from './getTopQueriesColumns'; +import {getTopQueriesColumns} from './columns/columns'; +import {TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} from './columns/constants'; import i18n from './i18n'; +import {TOP_QUERIES_TABLE_SETTINGS, useTopQueriesSort} from './utils'; interface Props { database: string; @@ -22,20 +22,24 @@ interface Props { export const TopQueriesData = ({database, onRowClick, rowClassName}: Props) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); - const {currentData, isLoading, error} = topQueriesApi.useGetTopQueriesQuery( + + const {tableSort, handleTableSort, backendSort} = useTopQueriesSort(); + + const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( { database, filters, + sortOrder: backendSort, }, {pollingInterval: autoRefreshInterval}, ); + const loading = isFetching && currentData === undefined; + const data = currentData?.resultSets?.[0]?.result || []; - const rawColumns = TOP_QUERIES_COLUMNS; - const columns = rawColumns.map((column) => ({ - ...column, - sortable: isSortableTopQueriesProperty(column.name), - })); + const columns = React.useMemo(() => { + return getTopQueriesColumns(); + }, []); const handleRowClick = (row: KeyValueRow) => { return onRowClick(row.QueryText as string); @@ -44,15 +48,17 @@ export const TopQueriesData = ({database, onRowClick, rowClassName}: Props) => { return ( {error ? : null} - + rowClassName} + sortOrder={tableSort} + onSort={handleTableSort} /> diff --git a/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx similarity index 56% rename from src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx rename to src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx index f4b305418..0b1f13904 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx @@ -4,26 +4,27 @@ import type {Column} from '@gravity-ui/react-data-table'; import { OneLineQueryWithPopover, TruncatedQuery, -} from '../../../../components/TruncatedQuery/TruncatedQuery'; -import type {KeyValueRow} from '../../../../types/api/query'; -import {cn} from '../../../../utils/cn'; -import {formatDateTime, formatNumber} from '../../../../utils/dataFormatters/dataFormatters'; -import {TOP_QUERIES_COLUMNS_IDS} from '../../../../utils/diagnostics'; -import {generateHash} from '../../../../utils/generateHash'; -import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; -import {MAX_QUERY_HEIGHT} from '../../utils/constants'; +} from '../../../../../components/TruncatedQuery/TruncatedQuery'; +import type {KeyValueRow} from '../../../../../types/api/query'; +import {cn} from '../../../../../utils/cn'; +import {formatDateTime, formatNumber} from '../../../../../utils/dataFormatters/dataFormatters'; +import {generateHash} from '../../../../../utils/generateHash'; +import {formatToMs, parseUsToMs} from '../../../../../utils/timeParsers'; +import {MAX_QUERY_HEIGHT} from '../../../utils/constants'; -import i18n from './i18n'; +import { + TOP_QUERIES_COLUMNS_IDS, + TOP_QUERIES_COLUMNS_TITLES, + isSortableTopQueriesColumn, +} from './constants'; -import './TopQueries.scss'; +import '../TopQueries.scss'; const b = cn('kv-top-queries'); -export const TOP_QUERIES_COLUMNS_WIDTH_LS_KEY = 'topQueriesColumnsWidth'; -export const RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY = 'runningQueriesColumnsWidth'; - const cpuTimeUsColumn: Column = { - name: TOP_QUERIES_COLUMNS_IDS.CPUTimeUs, + name: TOP_QUERIES_COLUMNS_IDS.CPUTime, + header: TOP_QUERIES_COLUMNS_TITLES.CPUTime, sortAccessor: (row) => Number(row.CPUTimeUs), render: ({row}) => formatToMs(parseUsToMs(row.CPUTimeUs ?? undefined)), width: 120, @@ -33,6 +34,7 @@ const cpuTimeUsColumn: Column = { const queryTextColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.QueryText, + header: TOP_QUERIES_COLUMNS_TITLES.QueryText, sortAccessor: (row) => Number(row.CPUTimeUs), render: ({row}) => (
@@ -45,6 +47,7 @@ const queryTextColumn: Column = { const endTimeColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.EndTime, + header: TOP_QUERIES_COLUMNS_TITLES.EndTime, render: ({row}) => formatDateTime(new Date(row.EndTime as string).getTime()), align: DataTable.RIGHT, width: 200, @@ -52,6 +55,7 @@ const endTimeColumn: Column = { const readRowsColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.ReadRows, + header: TOP_QUERIES_COLUMNS_TITLES.ReadRows, render: ({row}) => formatNumber(row.ReadRows), sortAccessor: (row) => Number(row.ReadRows), align: DataTable.RIGHT, @@ -60,6 +64,7 @@ const readRowsColumn: Column = { const readBytesColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.ReadBytes, + header: TOP_QUERIES_COLUMNS_TITLES.ReadBytes, render: ({row}) => formatNumber(row.ReadBytes), sortAccessor: (row) => Number(row.ReadBytes), align: DataTable.RIGHT, @@ -68,6 +73,7 @@ const readBytesColumn: Column = { const userSIDColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.UserSID, + header: TOP_QUERIES_COLUMNS_TITLES.UserSID, render: ({row}) =>
{row.UserSID || '–'}
, sortAccessor: (row) => String(row.UserSID), align: DataTable.LEFT, @@ -75,7 +81,7 @@ const userSIDColumn: Column = { const oneLineQueryTextColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.OneLineQueryText, - header: 'QueryText', + header: TOP_QUERIES_COLUMNS_TITLES.OneLineQueryText, render: ({row}) => , sortable: false, width: 500, @@ -83,6 +89,7 @@ const oneLineQueryTextColumn: Column = { const queryHashColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.QueryHash, + header: TOP_QUERIES_COLUMNS_TITLES.QueryHash, render: ({row}) => generateHash(String(row.QueryText)), width: 130, sortable: false, @@ -90,7 +97,7 @@ const queryHashColumn: Column = { const durationColumn: Column = { name: TOP_QUERIES_COLUMNS_IDS.Duration, - header: 'Duration', + header: TOP_QUERIES_COLUMNS_TITLES.Duration, render: ({row}) => formatToMs(parseUsToMs(row.Duration ?? undefined)), sortAccessor: (row) => Number(row.Duration), align: DataTable.RIGHT, @@ -98,10 +105,8 @@ const durationColumn: Column = { }; const queryStartColumn: Column = { - name: 'QueryStartAt', - get header() { - return i18n('col_start-time'); - }, + name: TOP_QUERIES_COLUMNS_IDS.QueryStartAt, + header: TOP_QUERIES_COLUMNS_TITLES.QueryStartAt, render: ({row}) => formatDateTime(new Date(row.QueryStartAt as string).getTime()), sortable: true, resizeable: false, @@ -109,34 +114,44 @@ const queryStartColumn: Column = { }; const applicationColumn: Column = { - name: 'ApplicationName', - get header() { - return i18n('col_app'); - }, + name: TOP_QUERIES_COLUMNS_IDS.ApplicationName, + header: TOP_QUERIES_COLUMNS_TITLES.ApplicationName, render: ({row}) =>
{row.ApplicationName || '–'}
, sortable: true, }; -export const TOP_QUERIES_COLUMNS = [ - queryHashColumn, - cpuTimeUsColumn, - queryTextColumn, - endTimeColumn, - durationColumn, - readRowsColumn, - readBytesColumn, - userSIDColumn, -]; - -export const TENANT_OVERVIEW_TOP_QUERUES_COLUMNS = [ - queryHashColumn, - oneLineQueryTextColumn, - cpuTimeUsColumn, -]; - -export const RUNNING_QUERIES_COLUMNS = [ - userSIDColumn, - queryStartColumn, - queryTextColumn, - applicationColumn, -]; +export function getTopQueriesColumns() { + const columns = [ + queryHashColumn, + cpuTimeUsColumn, + queryTextColumn, + endTimeColumn, + durationColumn, + readRowsColumn, + readBytesColumn, + userSIDColumn, + ]; + + return columns.map((column) => ({ + ...column, + sortable: isSortableTopQueriesColumn(column.name), + })); +} + +export function getTenantOverviewTopQueriesColumns() { + const columns = [queryHashColumn, oneLineQueryTextColumn, cpuTimeUsColumn]; + + return columns.map((column) => ({ + ...column, + sortable: false, + })); +} + +export function getRunningQueriesColumns() { + const columns = [userSIDColumn, queryStartColumn, queryTextColumn, applicationColumn]; + + return columns.map((column) => ({ + ...column, + sortable: isSortableTopQueriesColumn(column.name), + })); +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts b/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts new file mode 100644 index 000000000..4725e829e --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts @@ -0,0 +1,80 @@ +import type {ValueOf} from '../../../../../types/common'; + +import i18n from './i18n'; + +export const TOP_QUERIES_COLUMNS_WIDTH_LS_KEY = 'topQueriesColumnsWidth'; +export const RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY = 'runningQueriesColumnsWidth'; + +export const TOP_QUERIES_COLUMNS_IDS = { + CPUTime: 'CPUTime', + QueryText: 'QueryText', + EndTime: 'EndTime', + ReadRows: 'ReadRows', + ReadBytes: 'ReadBytes', + UserSID: 'UserSID', + OneLineQueryText: 'OneLineQueryText', + QueryHash: 'QueryHash', + Duration: 'Duration', + QueryStartAt: 'QueryStartAt', + ApplicationName: 'ApplicationName', +} as const; + +type TopQueriesColumnId = ValueOf; + +export const TOP_QUERIES_COLUMNS_TITLES: Record = { + get CPUTime() { + return i18n('cpu-time'); + }, + get QueryText() { + return i18n('query-text'); + }, + get EndTime() { + return i18n('end-time'); + }, + get ReadRows() { + return i18n('read-rows'); + }, + get ReadBytes() { + return i18n('read-bytes'); + }, + get UserSID() { + return i18n('user'); + }, + get OneLineQueryText() { + return i18n('query-text'); + }, + get QueryHash() { + return i18n('query-hash'); + }, + get Duration() { + return i18n('duration'); + }, + get QueryStartAt() { + return i18n('start-time'); + }, + get ApplicationName() { + return i18n('application'); + }, +} as const; + +const TOP_QUERIES_COLUMNS_TO_SORT_FIELDS: Record = { + CPUTime: 'CPUTimeUs', + QueryText: undefined, + EndTime: 'EndTime', + ReadRows: 'ReadRows', + ReadBytes: 'ReadBytes', + UserSID: 'UserSID', + OneLineQueryText: undefined, + QueryHash: undefined, + Duration: 'Duration', + QueryStartAt: 'QueryStartAt', + ApplicationName: 'ApplicationName', +} as const; + +export function getTopQueriesColumnSortField(columnId?: string) { + return TOP_QUERIES_COLUMNS_TO_SORT_FIELDS[columnId as TopQueriesColumnId]; +} + +export function isSortableTopQueriesColumn(columnId: string) { + return Boolean(getTopQueriesColumnSortField(columnId)); +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/columns/i18n/en.json new file mode 100644 index 000000000..760408803 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/i18n/en.json @@ -0,0 +1,12 @@ +{ + "cpu-time": "CPUTime", + "read-rows": "ReadRows", + "read-bytes": "ReadBytes", + "query-hash": "QueryHash", + "user": "User", + "start-time": "Start time", + "end-time": "End time", + "duration": "Duration", + "query-text": "Query text", + "application": "Application" +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/i18n/index.ts b/src/containers/Tenant/Diagnostics/TopQueries/columns/i18n/index.ts new file mode 100644 index 000000000..2adc2aa14 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-top-queries-columns'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json index 7ccee9bb8..cbdc83dc0 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -2,9 +2,5 @@ "no-data": "No data", "filter.text.placeholder": "Search by query text or userSID...", "mode_top": "Top", - "mode_running": "Running", - "col_user": "User", - "col_start-time": "Start time", - "col_query-text": "Query text", - "col_app": "Application" + "mode_running": "Running" } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts new file mode 100644 index 000000000..19126c690 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -0,0 +1,38 @@ +import React from 'react'; + +import type {Settings} from '@gravity-ui/react-data-table'; + +import {prepareBackendSortFieldsFromTableSort, useTableSort} from '../../../../utils/hooks'; +import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; + +import {TOP_QUERIES_COLUMNS_IDS, getTopQueriesColumnSortField} from './columns/constants'; + +export const TOP_QUERIES_TABLE_SETTINGS: Settings = { + ...QUERY_TABLE_SETTINGS, + disableSortReset: true, +}; + +function useQueriesSort(initialSortColumn: string) { + const [tableSort, handleTableSort] = useTableSort({ + initialSortColumn: initialSortColumn, + initialSortOrder: -1, + multiple: true, + }); + + const backendSort = React.useMemo( + () => prepareBackendSortFieldsFromTableSort(tableSort, getTopQueriesColumnSortField), + [tableSort], + ); + + return { + tableSort, + handleTableSort, + backendSort, + }; +} +export function useTopQueriesSort() { + return useQueriesSort(TOP_QUERIES_COLUMNS_IDS.CPUTime); +} +export function useRunningQueriesSort() { + return useQueriesSort(TOP_QUERIES_COLUMNS_IDS.QueryStartAt); +} diff --git a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx index 1850ac326..5b44dc462 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type {Column, Settings, SortOrder} from '@gravity-ui/react-data-table'; +import type {Column, Settings} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import {useLocation} from 'react-router-dom'; @@ -18,14 +18,19 @@ import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; import {formatDateTime} from '../../../../utils/dataFormatters/dataFormatters'; -import {TOP_SHARD_COLUMNS_IDS, isSortableTopShardsProperty} from '../../../../utils/diagnostics'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../utils/query'; import {isColumnEntityType} from '../../utils/schema'; import {Filters} from './Filters'; -import {TOP_SHARDS_COLUMNS_WIDTH_LS_KEY, getShardsWorkloadColumns} from './getTopShardsColumns'; +import {getShardsWorkloadColumns} from './columns/columns'; +import { + TOP_SHARDS_COLUMNS_IDS, + TOP_SHARDS_COLUMNS_WIDTH_LS_KEY, + isSortableTopShardsColumn, +} from './columns/constants'; import i18n from './i18n'; +import {useTopShardSort} from './utils'; import './TopShards.scss'; @@ -46,29 +51,6 @@ function prepareDateTimeValue(value: CellValue) { return formatDateTime(new Date(value).getTime()); } -function stringToDataTableSortOrder(value: string): SortOrder[] | undefined { - return value - ? value.split(',').map((columnId) => ({ - columnId, - order: DataTable.DESCENDING, - })) - : undefined; -} - -function stringToQuerySortOrder(value: string) { - return value - ? value.split(',').map((columnId) => ({ - columnId, - order: 'DESC', - })) - : undefined; -} - -function dataTableToStringSortOrder(value: SortOrder | SortOrder[] = []) { - const sortOrders = Array.isArray(value) ? value : [value]; - return sortOrders.map(({columnId}) => columnId).join(','); -} - function fillDateRangeFor(value: ShardsWorkloadFilters) { value.to = 'now'; value.from = 'now-1h'; @@ -105,7 +87,8 @@ export const TopShards = ({tenantName, path, type}: TopShardsProps) => { return defaultValue; }); - const [sortOrder, setSortOrder] = React.useState(TOP_SHARD_COLUMNS_IDS.CPUCores); + const {tableSort, handleTableSort, backendSort} = useTopShardSort(); + const { currentData: result, isFetching, @@ -114,7 +97,7 @@ export const TopShards = ({tenantName, path, type}: TopShardsProps) => { { database: tenantName, path: path, - sortOrder: stringToQuerySortOrder(sortOrder), + sortOrder: backendSort, filters, }, {pollingInterval: autoRefreshInterval}, @@ -122,13 +105,6 @@ export const TopShards = ({tenantName, path, type}: TopShardsProps) => { const loading = isFetching && result === undefined; const data = result?.resultSets?.[0]?.result || []; - const onSort = (newSortOrder?: SortOrder | SortOrder[]) => { - // omit information about sort order to disable ASC order, only DESC makes sense for top shards - // use a string (and not the DataTable default format) to prevent reference change, - // which would cause an excess state change, to avoid repeating requests - setSortOrder(dataTableToStringSortOrder(newSortOrder)); - }; - const handleFiltersChange = (value: Partial) => { const newStateValue = {...value}; const isDateRangePristine = @@ -155,20 +131,20 @@ export const TopShards = ({tenantName, path, type}: TopShardsProps) => { const columns: Column[] = rawColumns.map((column) => ({ ...column, - sortable: isSortableTopShardsProperty(column.name), + sortable: isSortableTopShardsColumn(column.name), })); if (filters.mode === EShardsWorkloadMode.History) { // after NodeId columns.splice(5, 0, { - name: TOP_SHARD_COLUMNS_IDS.PeakTime, + name: TOP_SHARDS_COLUMNS_IDS.PeakTime, render: ({row}) => { return prepareDateTimeValue(row.PeakTime); }, sortable: false, }); columns.push({ - name: TOP_SHARD_COLUMNS_IDS.IntervalEnd, + name: TOP_SHARDS_COLUMNS_IDS.IntervalEnd, render: ({row}) => { return prepareDateTimeValue(row.IntervalEnd); }, @@ -197,8 +173,8 @@ export const TopShards = ({tenantName, path, type}: TopShardsProps) => { columns={tableColumns} data={data} settings={TABLE_SETTINGS} - onSort={onSort} - sortOrder={stringToDataTableSortOrder(sortOrder)} + onSort={handleTableSort} + sortOrder={tableSort} /> ); }; diff --git a/src/containers/Tenant/Diagnostics/TopShards/getTopShardsColumns.tsx b/src/containers/Tenant/Diagnostics/TopShards/columns/columns.tsx similarity index 60% rename from src/containers/Tenant/Diagnostics/TopShards/getTopShardsColumns.tsx rename to src/containers/Tenant/Diagnostics/TopShards/columns/columns.tsx index b5774bcf9..70190234a 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/getTopShardsColumns.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/columns/columns.tsx @@ -2,41 +2,16 @@ import DataTable from '@gravity-ui/react-data-table'; import type {Column} from '@gravity-ui/react-data-table'; import type {Location} from 'history'; -import {InternalLink} from '../../../../components/InternalLink'; -import {LinkToSchemaObject} from '../../../../components/LinkToSchemaObject/LinkToSchemaObject'; -import {TabletNameWrapper} from '../../../../components/TabletNameWrapper/TabletNameWrapper'; -import {UsageLabel} from '../../../../components/UsageLabel/UsageLabel'; -import {getLoadSeverityForShard} from '../../../../store/reducers/tenantOverview/topShards/utils'; -import type {KeyValueRow} from '../../../../types/api/query'; -import type {ValueOf} from '../../../../types/common'; -import {formatNumber, roundToPrecision} from '../../../../utils/dataFormatters/dataFormatters'; -import {getDefaultNodePath} from '../../../Node/NodePages'; +import {InternalLink} from '../../../../../components/InternalLink'; +import {LinkToSchemaObject} from '../../../../../components/LinkToSchemaObject/LinkToSchemaObject'; +import {TabletNameWrapper} from '../../../../../components/TabletNameWrapper/TabletNameWrapper'; +import {UsageLabel} from '../../../../../components/UsageLabel/UsageLabel'; +import {getLoadSeverityForShard} from '../../../../../store/reducers/tenantOverview/topShards/utils'; +import type {KeyValueRow} from '../../../../../types/api/query'; +import {formatNumber, roundToPrecision} from '../../../../../utils/dataFormatters/dataFormatters'; +import {getDefaultNodePath} from '../../../../Node/NodePages'; -export const TOP_SHARDS_COLUMNS_WIDTH_LS_KEY = 'topShardsColumnsWidth'; - -const TOP_SHARDS_COLUMNS_IDS = { - TabletId: 'TabletId', - CPUCores: 'CPUCores', - DataSize: 'DataSize', - Path: 'Path', - NodeId: 'NodeId', - PeakTime: 'PeakTime', - InFlightTxCount: 'InFlightTxCount', - IntervalEnd: 'IntervalEnd', -} as const; - -type TopShardsColumns = ValueOf; - -const tableColumnsNames: Record = { - TabletId: 'TabletId', - CPUCores: 'CPUCores', - DataSize: 'DataSize (B)', - Path: 'Path', - NodeId: 'NodeId', - PeakTime: 'PeakTime', - InFlightTxCount: 'InFlightTxCount', - IntervalEnd: 'IntervalEnd', -}; +import {TOP_SHARDS_COLUMNS_IDS, TOP_SHARDS_COLUMNS_TITLES} from './constants'; function prepareCPUWorkloadValue(value: string | number) { return `${roundToPrecision(Number(value) * 100, 2)}%`; @@ -44,7 +19,7 @@ function prepareCPUWorkloadValue(value: string | number) { const getPathColumn = (schemaPath: string, location: Location): Column => ({ name: TOP_SHARDS_COLUMNS_IDS.Path, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.Path], + header: TOP_SHARDS_COLUMNS_TITLES.Path, render: ({row}) => { // row.Path - relative schema path return ( @@ -59,7 +34,7 @@ const getPathColumn = (schemaPath: string, location: Location): Column = { name: TOP_SHARDS_COLUMNS_IDS.CPUCores, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.CPUCores], + header: TOP_SHARDS_COLUMNS_TITLES.CPUCores, render: ({row}) => { return prepareCPUWorkloadValue(row.CPUCores || 0); }, @@ -68,7 +43,7 @@ const cpuCoresColumn: Column = { const dataSizeColumn: Column = { name: TOP_SHARDS_COLUMNS_IDS.DataSize, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.DataSize], + header: TOP_SHARDS_COLUMNS_TITLES.DataSize, render: ({row}) => { return formatNumber(row.DataSize); }, @@ -77,7 +52,7 @@ const dataSizeColumn: Column = { const tabletIdColumn: Column = { name: TOP_SHARDS_COLUMNS_IDS.TabletId, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.TabletId], + header: TOP_SHARDS_COLUMNS_TITLES.TabletId, render: ({row}) => { if (!row.TabletId) { return '–'; @@ -90,7 +65,7 @@ const tabletIdColumn: Column = { const nodeIdColumn: Column = { name: TOP_SHARDS_COLUMNS_IDS.NodeId, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.NodeId], + header: TOP_SHARDS_COLUMNS_TITLES.NodeId, render: ({row}) => { if (!row.NodeId) { return '–'; @@ -102,7 +77,7 @@ const nodeIdColumn: Column = { const topShardsCpuCoresColumn: Column = { name: TOP_SHARDS_COLUMNS_IDS.CPUCores, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.CPUCores], + header: TOP_SHARDS_COLUMNS_TITLES.CPUCores, render: ({row}) => { return ( = { const inFlightTxCountColumn: Column = { name: TOP_SHARDS_COLUMNS_IDS.InFlightTxCount, - header: tableColumnsNames[TOP_SHARDS_COLUMNS_IDS.InFlightTxCount], + header: TOP_SHARDS_COLUMNS_TITLES.InFlightTxCount, render: ({row}) => formatNumber(row.InFlightTxCount), align: DataTable.RIGHT, }; diff --git a/src/containers/Tenant/Diagnostics/TopShards/columns/constants.ts b/src/containers/Tenant/Diagnostics/TopShards/columns/constants.ts new file mode 100644 index 000000000..5be1db8b8 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopShards/columns/constants.ts @@ -0,0 +1,64 @@ +import type {ValueOf} from '../../../../../types/common'; + +import i18n from './i18n'; + +export const TOP_SHARDS_COLUMNS_WIDTH_LS_KEY = 'topShardsColumnsWidth'; + +export const TOP_SHARDS_COLUMNS_IDS = { + TabletId: 'TabletId', + CPUCores: 'CPUCores', + DataSize: 'DataSize', + Path: 'Path', + NodeId: 'NodeId', + PeakTime: 'PeakTime', + InFlightTxCount: 'InFlightTxCount', + IntervalEnd: 'IntervalEnd', +} as const; + +type TopShardsColumnId = ValueOf; + +export const TOP_SHARDS_COLUMNS_TITLES: Record = { + get TabletId() { + return i18n('tablet-id'); + }, + get CPUCores() { + return i18n('cpu-cores'); + }, + get DataSize() { + return i18n('data-size'); + }, + get Path() { + return i18n('path'); + }, + get NodeId() { + return i18n('node-id'); + }, + get PeakTime() { + return i18n('peak-time'); + }, + get InFlightTxCount() { + return i18n('in-flight-tx-count'); + }, + get IntervalEnd() { + return i18n('interval-end'); + }, +} as const; + +const TOP_SHARDS_COLUMNS_TO_SORT_FIELDS: Record = { + TabletId: undefined, + CPUCores: 'CPUCores', + DataSize: 'DataSize', + Path: undefined, + NodeId: undefined, + PeakTime: undefined, + InFlightTxCount: 'InFlightTxCount', + IntervalEnd: undefined, +} as const; + +export function getTopShardsColumnSortField(columnId?: string) { + return TOP_SHARDS_COLUMNS_TO_SORT_FIELDS[columnId as TopShardsColumnId]; +} + +export function isSortableTopShardsColumn(columnId: string) { + return Boolean(getTopShardsColumnSortField(columnId)); +} diff --git a/src/containers/Tenant/Diagnostics/TopShards/columns/i18n/en.json b/src/containers/Tenant/Diagnostics/TopShards/columns/i18n/en.json new file mode 100644 index 000000000..a82a9058b --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopShards/columns/i18n/en.json @@ -0,0 +1,10 @@ +{ + "tablet-id": "TabletId", + "cpu-cores": "CPUCores", + "data-size": "DataSize (B)", + "path": "Path", + "node-id": "NodeId", + "peak-time": "PeakTime", + "in-flight-tx-count": "InFlightTxCount", + "interval-end": "IntervalEnd" +} diff --git a/src/containers/Tenant/Diagnostics/TopShards/columns/i18n/index.ts b/src/containers/Tenant/Diagnostics/TopShards/columns/i18n/index.ts new file mode 100644 index 000000000..332949ec5 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopShards/columns/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-top-shards-columns'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/TopShards/utils.ts b/src/containers/Tenant/Diagnostics/TopShards/utils.ts new file mode 100644 index 000000000..f5b5b2059 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopShards/utils.ts @@ -0,0 +1,24 @@ +import React from 'react'; + +import {prepareBackendSortFieldsFromTableSort, useTableSort} from '../../../../utils/hooks'; + +import {TOP_SHARDS_COLUMNS_IDS, getTopShardsColumnSortField} from './columns/constants'; + +export function useTopShardSort() { + const [tableSort, handleTableSort] = useTableSort({ + initialSortColumn: TOP_SHARDS_COLUMNS_IDS.CPUCores, + fixedOrderType: -1, + multiple: true, + }); + + const backendSort = React.useMemo( + () => prepareBackendSortFieldsFromTableSort(tableSort, getTopShardsColumnSortField), + [tableSort], + ); + + return { + tableSort, + handleTableSort, + backendSort, + }; +} diff --git a/src/store/reducers/executeTopQueries/executeTopQueries.ts b/src/store/reducers/executeTopQueries/executeTopQueries.ts index 4e0837d50..7e99fae7a 100644 --- a/src/store/reducers/executeTopQueries/executeTopQueries.ts +++ b/src/store/reducers/executeTopQueries/executeTopQueries.ts @@ -1,7 +1,9 @@ import {isLikeRelative} from '@gravity-ui/date-utils'; +import type {SortOrder} from '@gravity-ui/react-data-table'; import {createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; +import {prepareOrderByFromTableSort} from '../../../utils/hooks/useTableSort'; import {isQueryErrorResponse, parseQueryAPIResponse} from '../../../utils/query'; import {api} from '../api'; @@ -25,8 +27,11 @@ const slice = createSlice({ export const {setTopQueriesFilters} = slice.actions; export default slice.reducer; -const getQueryText = (path: string, filters?: TopQueriesFilters) => { +const getQueryText = (path: string, filters?: TopQueriesFilters, sortOrder?: SortOrder[]) => { const filterConditions = getFiltersConditions(path, filters); + + const orderBy = prepareOrderByFromTableSort(sortOrder); + return ` SELECT ${QUERY_TECHNICAL_MARK} CPUTime as CPUTimeUs, @@ -39,17 +44,21 @@ SELECT ${QUERY_TECHNICAL_MARK} Duration FROM \`${path}/.sys/top_queries_by_cpu_time_one_hour\` WHERE ${filterConditions || 'true'} AND QueryText NOT LIKE '%${QUERY_TECHNICAL_MARK}%' -ORDER BY CPUTimeUs DESC +${orderBy} +LIMIT 100 `; }; +interface TopQueriesRequestParams { + database: string; + filters?: TopQueriesFilters; + sortOrder?: SortOrder[]; +} + export const topQueriesApi = api.injectEndpoints({ endpoints: (build) => ({ getTopQueries: build.query({ - queryFn: async ( - {database, filters}: {database: string; filters?: TopQueriesFilters}, - {signal}, - ) => { + queryFn: async ({database, filters, sortOrder}: TopQueriesRequestParams, {signal}) => { const preparedFilters = { ...filters, from: filters?.from || 'now-1h', @@ -59,7 +68,7 @@ export const topQueriesApi = api.injectEndpoints({ try { const response = await window.api.viewer.sendQuery( { - query: getQueryText(database, preparedFilters), + query: getQueryText(database, preparedFilters, sortOrder), database, action: 'execute-scan', }, @@ -91,23 +100,21 @@ export const topQueriesApi = api.injectEndpoints({ providesTags: ['All'], }), getRunningQueries: build.query({ - queryFn: async ( - {database, filters}: {database: string; filters?: TopQueriesFilters}, - {signal}, - ) => { + queryFn: async ({database, filters, sortOrder}: TopQueriesRequestParams, {signal}) => { try { const filterConditions = filters?.text ? `Query ILIKE '%${filters.text}%' OR UserSID ILIKE '%${filters.text}%'` : ''; + const orderBy = prepareOrderByFromTableSort(sortOrder); + const queryText = `SELECT ${QUERY_TECHNICAL_MARK} UserSID, QueryStartAt, Query as QueryText, ApplicationName FROM \`.sys/query_sessions\` WHERE ${filterConditions || 'true'} AND Query NOT LIKE '%${QUERY_TECHNICAL_MARK}%' - ORDER BY - SessionStartAt + ${orderBy} LIMIT 100`; const response = await window.api.viewer.sendQuery( diff --git a/src/store/reducers/nodes/types.ts b/src/store/reducers/nodes/types.ts index 92176d80d..b3def744b 100644 --- a/src/store/reducers/nodes/types.ts +++ b/src/store/reducers/nodes/types.ts @@ -51,8 +51,8 @@ export interface NodesPreparedEntity extends PreparedNodeSystemState { } export interface NodesSortParams { - sortOrder: OrderType | undefined; - sortValue: NodesSortValue | undefined; + sortOrder: OrderType; + sortValue: NodesSortValue; } export interface NodesFilters { diff --git a/src/store/reducers/shardsWorkload/shardsWorkload.ts b/src/store/reducers/shardsWorkload/shardsWorkload.ts index 4344dd0a7..a2da79903 100644 --- a/src/store/reducers/shardsWorkload/shardsWorkload.ts +++ b/src/store/reducers/shardsWorkload/shardsWorkload.ts @@ -1,7 +1,9 @@ import {dateTimeParse, isLikeRelative} from '@gravity-ui/date-utils'; +import type {SortOrder} from '@gravity-ui/react-data-table'; import {createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; +import {prepareOrderByFromTableSort} from '../../../utils/hooks/useTableSort'; import {isQueryErrorResponse, parseQueryAPIResponse} from '../../../utils/query'; import {api} from '../api'; @@ -10,15 +12,6 @@ import {EShardsWorkloadMode} from './types'; const initialState: ShardsWorkloadFilters = {}; -export interface SortOrder { - columnId: string; - order: string; -} - -function formatSortOrder({columnId, order}: SortOrder) { - return `${columnId} ${order}`; -} - function getFiltersConditions(filters?: ShardsWorkloadFilters) { const conditions: string[] = []; const to = dateTimeParse(Number(filters?.to) || filters?.to)?.valueOf(); @@ -59,7 +52,7 @@ function createShardQueryHistorical( where = `(${where}) AND ${filterConditions}`; } - const orderBy = sortOrder ? `ORDER BY ${sortOrder.map(formatSortOrder).join(', ')}` : ''; + const orderBy = prepareOrderByFromTableSort(sortOrder); return `SELECT ${pathSelect}, @@ -81,7 +74,7 @@ function createShardQueryImmediate(path: string, sortOrder?: SortOrder[], tenant ? `CAST(SUBSTRING(CAST(Path AS String), ${tenantName.length}) AS Utf8) AS Path` : 'Path'; - const orderBy = sortOrder ? `ORDER BY ${sortOrder.map(formatSortOrder).join(', ')}` : ''; + const orderBy = prepareOrderByFromTableSort(sortOrder); return `SELECT ${pathSelect}, diff --git a/src/store/reducers/storage/types.ts b/src/store/reducers/storage/types.ts index 476634fcb..f841c7d14 100644 --- a/src/store/reducers/storage/types.ts +++ b/src/store/reducers/storage/types.ts @@ -96,8 +96,8 @@ export interface PreparedStorageGroup { } export interface StorageSortParams { - sortOrder: OrderType | undefined; - sortValue: StorageV2SortValue | undefined; + sortOrder: OrderType; + sortValue: StorageV2SortValue; } export type TableGroup = { diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts deleted file mode 100644 index f3b3331de..000000000 --- a/src/utils/diagnostics.ts +++ /dev/null @@ -1,43 +0,0 @@ -export const TOP_QUERIES_COLUMNS_IDS = { - CPUTimeUs: 'CPUTime', - QueryText: 'QueryText', - EndTime: 'EndTime', - ReadRows: 'ReadRows', - ReadBytes: 'ReadBytes', - UserSID: 'UserSID', - OneLineQueryText: 'OneLineQueryText', - QueryHash: 'QueryHash', - Duration: 'Duration', -}; - -export const TOP_SHARD_COLUMNS_IDS = { - TabletId: 'TabletId', - CPUCores: 'CPUCores', - DataSize: 'DataSize', - Path: 'Path', - NodeId: 'NodeId', - PeakTime: 'PeakTime', - InFlightTxCount: 'InFlightTxCount', - IntervalEnd: 'IntervalEnd', -}; - -const TOP_SHARDS_SORT_VALUES = [ - TOP_SHARD_COLUMNS_IDS.CPUCores, - TOP_SHARD_COLUMNS_IDS.DataSize, - TOP_SHARD_COLUMNS_IDS.InFlightTxCount, -]; - -const TOP_QUERIES_SORT_VALUES = [ - TOP_QUERIES_COLUMNS_IDS.CPUTimeUs, - TOP_QUERIES_COLUMNS_IDS.EndTime, - TOP_QUERIES_COLUMNS_IDS.ReadRows, - TOP_QUERIES_COLUMNS_IDS.ReadBytes, - TOP_QUERIES_COLUMNS_IDS.UserSID, - TOP_QUERIES_COLUMNS_IDS.Duration, -]; - -export const isSortableTopShardsProperty = (value: string) => - Object.values(TOP_SHARDS_SORT_VALUES).includes(value); - -export const isSortableTopQueriesProperty = (value: string) => - Object.values(TOP_QUERIES_SORT_VALUES).includes(value); diff --git a/src/utils/hooks/__test__/useTableSort.test.ts b/src/utils/hooks/__test__/useTableSort.test.ts new file mode 100644 index 000000000..8b80395bd --- /dev/null +++ b/src/utils/hooks/__test__/useTableSort.test.ts @@ -0,0 +1,226 @@ +import React from 'react'; + +import {renderHook} from '@testing-library/react'; + +import {useTableSort} from '../useTableSort'; + +describe('useTableSort', function () { + it('It works with single sort', () => { + const onSortSpy = jest.fn(); + const {result} = renderHook(() => + useTableSort({ + initialSortColumn: 'col1', + initialSortOrder: 1, + onSort: onSortSpy, + }), + ); + const [sortOrder, handleTableSort] = result.current; + expect(sortOrder).toStrictEqual([ + { + columnId: 'col1', + order: 1, + }, + ]); + + // Change sort order + React.act(() => + handleTableSort([ + { + columnId: 'col1', + order: -1, + }, + ]), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col1', + order: -1, + }, + ]); + + // Reset sort order + React.act(() => handleTableSort(undefined)); + expect(onSortSpy).toHaveBeenLastCalledWith(undefined); + + // Sort another column + React.act(() => + handleTableSort({ + columnId: 'col2', + order: 1, + }), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col2', + order: 1, + }, + ]); + + // Sort multiple columns + React.act(() => + handleTableSort([ + { + columnId: 'col1', + order: -1, + }, + { + columnId: 'col2', + order: 1, + }, + ]), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col1', + order: -1, + }, + ]); + }); + it('It works with multiple sort', () => { + const onSortSpy = jest.fn(); + const {result} = renderHook(() => + useTableSort({ + initialSortColumn: 'col1', + initialSortOrder: 1, + multiple: true, + onSort: onSortSpy, + }), + ); + const [sortOrder, handleTableSort] = result.current; + expect(sortOrder).toStrictEqual([ + { + columnId: 'col1', + order: 1, + }, + ]); + + // Change sort order + React.act(() => + handleTableSort([ + { + columnId: 'col1', + order: -1, + }, + ]), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col1', + order: -1, + }, + ]); + + // Reset sort order + React.act(() => handleTableSort(undefined)); + expect(onSortSpy).toHaveBeenLastCalledWith(undefined); + + // Sort another column + React.act(() => + handleTableSort({ + columnId: 'col2', + order: 1, + }), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col2', + order: 1, + }, + ]); + + // Sort multiple columns + React.act(() => + handleTableSort([ + { + columnId: 'col1', + order: -1, + }, + { + columnId: 'col2', + order: 1, + }, + ]), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col1', + order: -1, + }, + { + columnId: 'col2', + order: 1, + }, + ]); + }); + it('Have the same sort order if sort order is fixed', () => { + const onSortSpy = jest.fn(); + const {result} = renderHook(() => + useTableSort({ + initialSortColumn: 'col1', + fixedOrderType: -1, + onSort: onSortSpy, + multiple: true, + }), + ); + const [sortOrder, handleTableSort] = result.current; + expect(sortOrder).toStrictEqual([ + { + columnId: 'col1', + order: -1, + }, + ]); + + // Change sort order, sort should remain DESC + React.act(() => + handleTableSort([ + { + columnId: 'col1', + order: 1, + }, + ]), + ); + expect(onSortSpy).not.toHaveBeenCalled(); + + // Reset sort order, sort should remain DESC + React.act(() => handleTableSort(undefined)); + expect(onSortSpy).not.toHaveBeenCalled(); + + // Sort another column + React.act(() => + handleTableSort({ + columnId: 'col2', + order: 1, + }), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col2', + order: -1, + }, + ]); + + // Sort multiple columns + React.act(() => + handleTableSort([ + { + columnId: 'col1', + order: -1, + }, + { + columnId: 'col2', + order: 1, + }, + ]), + ); + expect(onSortSpy).toHaveBeenLastCalledWith([ + { + columnId: 'col1', + order: -1, + }, + { + columnId: 'col2', + order: -1, + }, + ]); + }); +}); diff --git a/src/utils/hooks/useTableSort.ts b/src/utils/hooks/useTableSort.ts index e5daeb08c..97fc8586c 100644 --- a/src/utils/hooks/useTableSort.ts +++ b/src/utils/hooks/useTableSort.ts @@ -1,37 +1,104 @@ import React from 'react'; import type {OrderType, SortOrder} from '@gravity-ui/react-data-table'; -import {DESCENDING} from '@gravity-ui/react-data-table/build/esm/lib/constants'; - -interface SortParams { - sortValue: string | undefined; - sortOrder: OrderType | undefined; -} +import isEqual from 'lodash/isEqual'; export type HandleSort = (rawValue: SortOrder | SortOrder[] | undefined) => void; -export const useTableSort = ( - {sortValue, sortOrder = DESCENDING}: SortParams, - onSort: (params: SortParams) => void, -): [SortOrder | undefined, HandleSort] => { - const sort: SortOrder | undefined = React.useMemo(() => { - if (!sortValue) { +interface TableSortProps { + initialSortColumn?: string; + initialSortOrder?: OrderType; + multiple?: boolean; + fixedOrderType?: OrderType; + onSort?: (params: SortOrder[] | undefined) => void; +} + +export function useTableSort({ + initialSortColumn, + initialSortOrder = -1, + fixedOrderType, + multiple, + onSort, +}: TableSortProps): [SortOrder[] | undefined, HandleSort] { + const [sortOrder, setSortOrder] = React.useState(() => { + if (!initialSortColumn) { return undefined; } + return [ + { + columnId: initialSortColumn, + order: fixedOrderType ? fixedOrderType : initialSortOrder, + }, + ]; + }); + + const handleTableSort: HandleSort = React.useCallback( + (rawValue) => { + if (!rawValue || (Array.isArray(rawValue) && !rawValue.length)) { + // In case we have fixedOrderType, we should not reset table sort to undefined, but use previously set order + if (!fixedOrderType) { + onSort?.(undefined); + setSortOrder(undefined); + } + return; + } + + let newSortOrder: SortOrder[] = Array.isArray(rawValue) ? rawValue : [rawValue]; + + if (fixedOrderType) { + newSortOrder = newSortOrder.map((value) => { + return { + columnId: value.columnId, + order: fixedOrderType, + }; + }); + } + + if (!multiple) { + newSortOrder = newSortOrder.slice(0, 1); + } + + setSortOrder((currentSortOrder) => { + if (newSortOrder && !isEqual(currentSortOrder, newSortOrder)) { + onSort?.(newSortOrder); + + return newSortOrder; + } - return { - columnId: sortValue, - order: sortOrder, - }; - }, [sortValue, sortOrder]); - - const handleSort: HandleSort = (rawValue) => { - const value = Array.isArray(rawValue) ? rawValue[0] : rawValue; - onSort({ - sortValue: value?.columnId, - sortOrder: value?.order, - }); - }; - - return [sort, handleSort]; -}; + return currentSortOrder; + }); + }, + [fixedOrderType, multiple, onSort], + ); + + return [sortOrder, handleTableSort]; +} + +export function prepareBackendSortFieldsFromTableSort( + sortOrder: SortOrder[] = [], + getSortFieldFromColumnId: (columnId: string) => string | undefined, +): SortOrder[] | undefined { + const preparedSort = sortOrder + .map((value) => { + return { + columnId: getSortFieldFromColumnId(value.columnId), + order: value.order, + }; + }) + .filter((value): value is SortOrder => Boolean(value.columnId)); + + if (preparedSort.length) { + return preparedSort; + } + return undefined; +} + +function formatSortOrderToQuerySort({columnId, order}: SortOrder) { + const queryOrder = order === -1 ? 'DESC' : 'ASC'; + + return `${columnId} ${queryOrder}`; +} + +export function prepareOrderByFromTableSort(sortOrder?: SortOrder[]) { + return sortOrder ? `ORDER BY ${sortOrder.map(formatSortOrderToQuerySort).join(', ')}` : ''; +} diff --git a/tests/suites/tenant/diagnostics/diagnostics.test.ts b/tests/suites/tenant/diagnostics/diagnostics.test.ts index 8591324de..f2b5be6cf 100644 --- a/tests/suites/tenant/diagnostics/diagnostics.test.ts +++ b/tests/suites/tenant/diagnostics/diagnostics.test.ts @@ -66,7 +66,7 @@ test.describe('Diagnostics tab', async () => { await diagnostics.clickTab(DiagnosticsTab.Queries); await diagnostics.clickRadioSwitch(QueriesSwitch.Running); expect( - await diagnostics.table.waitForCellValueByHeader(1, 'QueryText', longRunningQuery), + await diagnostics.table.waitForCellValueByHeader(1, 'Query text', longRunningQuery), ).toBe(true); }); });