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');
+};