diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 645e88f7e..592e7948c 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -93,7 +93,7 @@ function Diagnostics(props: DiagnosticsProps) { return ; } case TENANT_DIAGNOSTICS_TABS_IDS.topQueries: { - return ; + return ; } case TENANT_DIAGNOSTICS_TABS_IDS.topShards: { return ; diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts index b60b04839..6400ec119 100644 --- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts +++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts @@ -19,7 +19,7 @@ const schema = { const topQueries = { id: TENANT_DIAGNOSTICS_TABS_IDS.topQueries, - title: 'Top queries', + title: 'Queries', }; const topShards = { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx new file mode 100644 index 000000000..9a40f8a58 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import {ResponseError} from '../../../../components/Errors/ResponseError'; +import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; +import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; +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 i18n from './i18n'; + +interface Props { + database: string; +} + +export const RunningQueriesData = ({database}: Props) => { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const filters = useTypedSelector((state) => state.executeTopQueries); + const { + currentData: data, + isFetching, + error, + } = topQueriesApi.useGetRunningQueriesQuery( + { + database, + filters, + }, + {pollingInterval: autoRefreshInterval}, + ); + + return ( + + {error ? : null} + + + + + ); +}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index 807b18c3a..8cbdd635d 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -1,73 +1,75 @@ import React from 'react'; +import type {RadioButtonOption} from '@gravity-ui/uikit'; +import {RadioButton} from '@gravity-ui/uikit'; import {useHistory, useLocation} from 'react-router-dom'; +import {StringParam, useQueryParam} from 'use-query-params'; +import {z} from 'zod'; import type {DateRangeValues} from '../../../../components/DateRange'; import {DateRange} from '../../../../components/DateRange'; -import {ResponseError} from '../../../../components/Errors/ResponseError'; -import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {parseQuery} from '../../../../routes'; import {changeUserInput} from '../../../../store/reducers/executeQuery'; -import { - setTopQueriesFilters, - topQueriesApi, -} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; +import {setTopQueriesFilters} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; import { TENANT_PAGE, TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID, } from '../../../../store/reducers/tenant/constants'; -import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; -import {isSortableTopQueriesProperty} from '../../../../utils/diagnostics'; -import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; -import {parseQueryErrorToString} from '../../../../utils/query'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; -import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; -import {isColumnEntityType} from '../../utils/schema'; -import {TOP_QUERIES_COLUMNS, TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} from './getTopQueriesColumns'; +import {RunningQueriesData} from './RunningQueriesData'; +import {TopQueriesData} from './TopQueriesData'; import i18n from './i18n'; import './TopQueries.scss'; const b = cn('kv-top-queries'); +const QueryModeIds = { + top: 'top', + running: 'running', +} as const; + +const QUERY_MODE_OPTIONS: RadioButtonOption[] = [ + { + value: QueryModeIds.top, + get content() { + return i18n('mode_top'); + }, + }, + { + value: QueryModeIds.running, + get content() { + return i18n('mode_running'); + }, + }, +]; + +const queryModeSchema = z.nativeEnum(QueryModeIds).catch(QueryModeIds.top); + interface TopQueriesProps { tenantName: string; - type?: EPathType; } -export const TopQueries = ({tenantName, type}: TopQueriesProps) => { +export const TopQueries = ({tenantName}: TopQueriesProps) => { const dispatch = useTypedDispatch(); const location = useLocation(); const history = useHistory(); + const [_queryMode = QueryModeIds.top, setQueryMode] = useQueryParam('queryMode', StringParam); - const [autoRefreshInterval] = useAutoRefreshInterval(); - - const filters = useTypedSelector((state) => state.executeTopQueries); - const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( - { - database: tenantName, - filters, - }, - {pollingInterval: autoRefreshInterval}, - ); - const loading = isFetching && currentData === undefined; - const {result: data} = currentData || {}; + const queryMode = queryModeSchema.parse(_queryMode); - const rawColumns = TOP_QUERIES_COLUMNS; - const columns = rawColumns.map((column) => ({ - ...column, - sortable: isSortableTopQueriesProperty(column.name), - })); + const isTopQueries = queryMode === QueryModeIds.top; - const handleRowClick = React.useCallback( - (row: any) => { - const {QueryText: input} = row; + const filters = useTypedSelector((state) => state.executeTopQueries); + const onRowClick = React.useCallback( + (input: string) => { dispatch(changeUserInput({input})); const queryParams = parseQuery(location); @@ -91,48 +93,33 @@ export const TopQueries = ({tenantName, type}: TopQueriesProps) => { dispatch(setTopQueriesFilters(value)); }; - const renderContent = () => { - if (error && !data) { - return null; - } - - if (!data || isColumnEntityType(type)) { - return i18n('no-data'); - } - - return ( - b('row')} - /> - ); - }; - - const renderControls = () => { - return ( - + return ( + + + - - - ); - }; - - return ( - - {renderControls()} - {error ? : null} - - {renderContent()} - + {isTopQueries ? ( + + ) : null} + + {isTopQueries ? ( + + ) : ( + + )} ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx new file mode 100644 index 000000000..da3aa82a8 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import {ResponseError} from '../../../../components/Errors/ResponseError'; +import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; +import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; +import type {KeyValueRow} from '../../../../types/api/query'; +import {cn} from '../../../../utils/cn'; +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 i18n from './i18n'; + +const b = cn('kv-top-queries'); + +interface Props { + database: string; + onRowClick: (query: string) => void; +} + +export const TopQueriesData = ({database, onRowClick}: Props) => { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const filters = useTypedSelector((state) => state.executeTopQueries); + const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( + { + database, + filters, + }, + {pollingInterval: autoRefreshInterval}, + ); + const {result: data} = currentData || {}; + + const rawColumns = TOP_QUERIES_COLUMNS; + const columns = rawColumns.map((column) => ({ + ...column, + sortable: isSortableTopQueriesProperty(column.name), + })); + + const handleRowClick = (row: KeyValueRow) => { + return onRowClick(row.QueryText as string); + }; + + return ( + + {error ? : null} + + b('row')} + /> + + + ); +}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx index 6cbbffa10..93a7c9a9d 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/getTopQueriesColumns.tsx @@ -13,11 +13,14 @@ import {generateHash} from '../../../../utils/generateHash'; import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; import {MAX_QUERY_HEIGHT} from '../../utils/constants'; +import i18n from './i18n'; + 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, @@ -94,6 +97,26 @@ const durationColumn: Column = { width: 150, }; +const queryStartColumn: Column = { + name: 'QueryStartAt', + get header() { + return i18n('col_start-time'); + }, + render: ({row}) => formatDateTime(new Date(row.QueryStartAt as string).getTime()), + sortable: true, + resizeable: false, + defaultOrder: DataTable.DESCENDING, +}; + +const applicationColumn: Column = { + name: 'ApplicationName', + get header() { + return i18n('col_app'); + }, + render: ({row}) =>
{row.ApplicationName || '–'}
, + sortable: true, +}; + export const TOP_QUERIES_COLUMNS = [ cpuTimeUsColumn, queryTextColumn, @@ -109,3 +132,10 @@ export const TENANT_OVERVIEW_TOP_QUERUES_COLUMNS = [ oneLineQueryTextColumn, cpuTimeUsColumn, ]; + +export const RUNNING_QUERIES_COLUMNS = [ + userSIDColumn, + queryStartColumn, + queryTextColumn, + applicationColumn, +]; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json index 7674260b1..43ef7d1c0 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -1,4 +1,10 @@ { "no-data": "No data", - "filter.text.placeholder": "Search by query text..." + "filter.text.placeholder": "Search by query text...", + "mode_top": "Top", + "mode_running": "Running", + "col_user": "User", + "col_start-time": "Start time", + "col_query-text": "Query text", + "col_app": "Application" } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/index.ts b/src/containers/Tenant/Diagnostics/TopQueries/i18n/index.ts index d4b0035c7..891dbe947 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/index.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/index.ts @@ -1,8 +1,7 @@ import {registerKeysets} from '../../../../../utils/i18n'; import en from './en.json'; -import ru from './ru.json'; const COMPONENT = 'ydb-diagnostics-top-queries'; -export default registerKeysets(COMPONENT, {ru, en}); +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/ru.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/ru.json deleted file mode 100644 index 90aeb4eda..000000000 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/ru.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "no-data": "Нет данных", - "filter.text.placeholder": "Искать по тексту запроса..." -} diff --git a/src/store/reducers/executeTopQueries/executeTopQueries.ts b/src/store/reducers/executeTopQueries/executeTopQueries.ts index 6b096d421..5dceeb70d 100644 --- a/src/store/reducers/executeTopQueries/executeTopQueries.ts +++ b/src/store/reducers/executeTopQueries/executeTopQueries.ts @@ -66,7 +66,7 @@ export const topQueriesApi = api.injectEndpoints({ ); if (isQueryErrorResponse(response)) { - return {error: response}; + throw response; } const data = parseQueryAPIExecuteResponse(response); @@ -87,6 +87,39 @@ export const topQueriesApi = api.injectEndpoints({ return false; }, + providesTags: ['All'], + }), + getRunningQueries: build.query({ + queryFn: async ( + {database, filters}: {database: string; filters?: TopQueriesFilters}, + {signal}, + ) => { + try { + const filterConditions = filters?.text ? `Query ILIKE '%${filters.text}%'` : ''; + const queryText = `SELECT UserSID, QueryStartAt, Query as QueryText, ApplicationName from \`.sys/query_sessions\` WHERE ${filterConditions || 'true'} ORDER BY SessionStartAt limit 100`; + + const response = await window.api.sendQuery( + { + query: queryText, + database, + action: 'execute-scan', + }, + {signal, withRetries: true}, + ); + + if (isQueryErrorResponse(response)) { + throw response; + } + + return {data: response?.result?.filter((item) => item.QueryText !== queryText)}; + } catch (error) { + return {error}; + } + }, + forceRefetch() { + return true; + }, + providesTags: ['All'], }), }), overrideExisting: 'throw',