diff --git a/package-lock.json b/package-lock.json index 15c5b2a34..d72a85cb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9686,6 +9686,11 @@ "yaml": "^1.10.0" } }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, "create-react-class": { "version": "15.7.0", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", diff --git a/package.json b/package.json index 0413c952b..6a5fc46e7 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "axios": "0.19.2", "bem-cn-lite": "4.0.0", "copy-to-clipboard": "^3.3.3", + "crc-32": "^1.2.2", "history": "4.10.1", "js-cookie": "2.2.1", "lodash": "4.17.11", diff --git a/src/components/TruncatedQuery/TruncatedQuery.scss b/src/components/TruncatedQuery/TruncatedQuery.scss index a34563f4d..6dcf7c6b5 100644 --- a/src/components/TruncatedQuery/TruncatedQuery.scss +++ b/src/components/TruncatedQuery/TruncatedQuery.scss @@ -14,4 +14,12 @@ } } } + + &__popover-content { + overflow: hidden; + + max-width: 600px; + + white-space: pre; + } } diff --git a/src/components/TruncatedQuery/TruncatedQuery.tsx b/src/components/TruncatedQuery/TruncatedQuery.tsx index 98851b862..bedc8977d 100644 --- a/src/components/TruncatedQuery/TruncatedQuery.tsx +++ b/src/components/TruncatedQuery/TruncatedQuery.tsx @@ -1,11 +1,13 @@ import cn from 'bem-cn-lite'; +import {CellWithPopover} from '../CellWithPopover/CellWithPopover'; + import './TruncatedQuery.scss'; const b = cn('kv-truncated-query'); interface TruncatedQueryProps { - value: string | undefined; + value?: string; maxQueryHeight?: number; } @@ -26,3 +28,15 @@ export const TruncatedQuery = ({value = '', maxQueryHeight = 6}: TruncatedQueryP } return <>{value}; }; + +interface OneLineQueryWithPopoverProps { + value?: string; +} + +export const OneLineQueryWithPopover = ({value = ''}: OneLineQueryWithPopoverProps) => { + return ( + + {value} + + ); +}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index 254bc697c..48914048d 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -1,4 +1,3 @@ -import qs from 'qs'; import {useDispatch} from 'react-redux'; import {useHistory, useLocation} from 'react-router'; import {useCallback} from 'react'; @@ -14,6 +13,7 @@ import { } from '../../../../../store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries'; import {changeUserInput} from '../../../../../store/reducers/executeQuery'; import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks'; +import {parseQuery} from '../../../../../routes'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import {getTenantOverviewTopQueriesColumns} from '../../TopQueries/getTopQueriesColumns'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; @@ -55,9 +55,7 @@ export function TopQueries({path}: TopQueriesProps) { dispatch(changeUserInput({input})); - const queryParams = qs.parse(location.search, { - ignoreQueryPrefix: true, - }); + const queryParams = parseQuery(location); const queryPath = getTenantPath({ ...queryParams, diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index 027ce98fd..241aaa191 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -30,6 +30,7 @@ import {useAutofetcher, useTypedSelector} from '../../../../utils/hooks'; import {prepareQueryError} from '../../../../utils/query'; import {parseQuery} from '../../../../routes'; import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; +import {isSortableTopQueriesProperty} from '../../../../utils/diagnostics'; import {isColumnEntityType} from '../../utils/schema'; import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; import {getTopQueriesColumns} from './getTopQueriesColumns'; @@ -58,7 +59,7 @@ export const TopQueries = ({path, type}: TopQueriesProps) => { data: {result: data = undefined} = {}, filters: storeFilters, } = useTypedSelector((state) => state.executeTopQueries); - const columns = getTopQueriesColumns(); + const rawColumns = getTopQueriesColumns(); const preventFetch = useRef(false); @@ -71,6 +72,11 @@ export const TopQueries = ({path, type}: TopQueriesProps) => { dispatch(setTopQueriesFilters(filters)); }, [dispatch, filters]); + const columns = rawColumns.map((column) => ({ + ...column, + sortable: isSortableTopQueriesProperty(column.name), + })); + const setDefaultFiltersFromResponse = (responseData?: IQueryResult) => { const intervalEnd = responseData?.result?.[0]?.IntervalEnd; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx index 7e3ee97d0..64b8386c8 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx @@ -4,7 +4,11 @@ import DataTable, {type Column} from '@gravity-ui/react-data-table'; import type {KeyValueRow} from '../../../../types/api/query'; import {formatDateTime, formatNumber} from '../../../../utils/dataFormatters/dataFormatters'; -import {TruncatedQuery} from '../../../../components/TruncatedQuery/TruncatedQuery'; +import {generateHash} from '../../../../utils/generateHash'; +import { + TruncatedQuery, + OneLineQueryWithPopover, +} from '../../../../components/TruncatedQuery/TruncatedQuery'; import {MAX_QUERY_HEIGHT} from '../../utils/constants'; import './TopQueries.scss'; @@ -18,6 +22,8 @@ const TOP_QUERIES_COLUMNS_IDS = { ReadRows: 'ReadRows', ReadBytes: 'ReadBytes', UserSID: 'UserSID', + OneLineQueryText: 'OneLineQueryText', + QueryHash: 'QueryHash', }; const cpuTimeUsColumn: Column = { @@ -66,6 +72,20 @@ const userSIDColumn: Column = { align: DataTable.LEFT, }; +const oneLineQueryTextColumn: Column = { + name: TOP_QUERIES_COLUMNS_IDS.OneLineQueryText, + header: 'QueryText', + render: ({row}) => , + sortable: false, +}; + +const queryHashColumn: Column = { + name: TOP_QUERIES_COLUMNS_IDS.QueryHash, + render: ({row}) => generateHash(String(row.QueryText)), + width: 130, + sortable: false, +}; + export const getTopQueriesColumns = (): Column[] => { return [ cpuTimeUsColumn, @@ -78,5 +98,5 @@ export const getTopQueriesColumns = (): Column[] => { }; export const getTenantOverviewTopQueriesColumns = (): Column[] => { - return [queryTextColumn, cpuTimeUsColumn]; + return [queryHashColumn, oneLineQueryTextColumn, cpuTimeUsColumn]; }; diff --git a/src/utils/diagnostics.ts b/src/utils/diagnostics.ts index 410127d94..44c634d53 100644 --- a/src/utils/diagnostics.ts +++ b/src/utils/diagnostics.ts @@ -1,11 +1,23 @@ import {ValueOf} from '../types/common'; -export const TOP_SHARDS_SORT_VALUES = { +const TOP_SHARDS_SORT_VALUES = { CPUCores: 'CPUCores', DataSize: 'DataSize', } as const; -export type TopShardsSortValue = ValueOf; +const TOP_QUERIES_SORT_VALUES = { + CPUTimeUs: 'CPUTimeUs', + EndTime: 'EndTime', + ReadRows: 'ReadRows', + ReadBytes: 'ReadBytes', + UserSID: 'UserSID', +} as const; + +type TopShardsSortValue = ValueOf; +type TopQueriesSortValue = ValueOf; export const isSortableTopShardsProperty = (value: string): value is TopShardsSortValue => Object.values(TOP_SHARDS_SORT_VALUES).includes(value as TopShardsSortValue); + +export const isSortableTopQueriesProperty = (value: string): value is TopQueriesSortValue => + Object.values(TOP_QUERIES_SORT_VALUES).includes(value as TopQueriesSortValue); diff --git a/src/utils/generateHash.ts b/src/utils/generateHash.ts new file mode 100644 index 000000000..cf20d6e5b --- /dev/null +++ b/src/utils/generateHash.ts @@ -0,0 +1,11 @@ +import crc32 from 'crc-32'; + +export const generateHash = (value: string) => { + // 1. crc32.str(value) - generate crc32 hash + // 2. (>>>) - use unsigned right shift operator (>>>) to avoid negative values + // 3. toString(16) - convert hash to hex format + // 4. toUpperCase() - convert hash to uppercase + // 5. padStart(8, '0') - fill hash with leading zeros if hash length < 8 + // eslint-disable-next-line no-bitwise + return (crc32.str(value) >>> 0).toString(16).toUpperCase().padStart(8, '0'); +};