Skip to content

Commit

Permalink
feat: show running queries (#1313)
Browse files Browse the repository at this point in the history
  • Loading branch information
sareyu authored Sep 25, 2024
1 parent 56839bc commit acd4a1e
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 81 deletions.
2 changes: 1 addition & 1 deletion src/containers/Tenant/Diagnostics/Diagnostics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function Diagnostics(props: DiagnosticsProps) {
return <SchemaViewer path={path} tenantName={tenantName} type={type} extended />;
}
case TENANT_DIAGNOSTICS_TABS_IDS.topQueries: {
return <TopQueries tenantName={tenantName} type={type} />;
return <TopQueries tenantName={tenantName} />;
}
case TENANT_DIAGNOSTICS_TABS_IDS.topShards: {
return <TopShards tenantName={tenantName} path={path} type={type} />;
Expand Down
2 changes: 1 addition & 1 deletion src/containers/Tenant/Diagnostics/DiagnosticsPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const schema = {

const topQueries = {
id: TENANT_DIAGNOSTICS_TABS_IDS.topQueries,
title: 'Top queries',
title: 'Queries',
};

const topShards = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<React.Fragment>
{error ? <ResponseError error={parseQueryErrorToString(error)} /> : null}
<TableWithControlsLayout.Table loading={isFetching && data === undefined}>
<ResizeableDataTable
emptyDataMessage={i18n('no-data')}
columnsWidthLSKey={RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY}
columns={RUNNING_QUERIES_COLUMNS}
data={data || []}
settings={QUERY_TABLE_SETTINGS}
/>
</TableWithControlsLayout.Table>
</React.Fragment>
);
};
129 changes: 58 additions & 71 deletions src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 (
<ResizeableDataTable
columnsWidthLSKey={TOP_QUERIES_COLUMNS_WIDTH_LS_KEY}
columns={columns}
data={data}
settings={QUERY_TABLE_SETTINGS}
onRowClick={handleRowClick}
rowClassName={() => b('row')}
/>
);
};

const renderControls = () => {
return (
<React.Fragment>
return (
<TableWithControlsLayout>
<TableWithControlsLayout.Controls>
<RadioButton
options={QUERY_MODE_OPTIONS}
value={queryMode}
onUpdate={setQueryMode}
/>
<Search
value={filters.text}
onChange={handleTextSearchUpdate}
placeholder={i18n('filter.text.placeholder')}
className={b('search')}
/>
<DateRange from={filters.from} to={filters.to} onChange={handleDateRangeChange} />
</React.Fragment>
);
};

return (
<TableWithControlsLayout>
<TableWithControlsLayout.Controls>{renderControls()}</TableWithControlsLayout.Controls>
{error ? <ResponseError error={parseQueryErrorToString(error)} /> : null}
<TableWithControlsLayout.Table loading={loading}>
{renderContent()}
</TableWithControlsLayout.Table>
{isTopQueries ? (
<DateRange
from={filters.from}
to={filters.to}
onChange={handleDateRangeChange}
/>
) : null}
</TableWithControlsLayout.Controls>
{isTopQueries ? (
<TopQueriesData database={tenantName} onRowClick={onRowClick} />
) : (
<RunningQueriesData database={tenantName} />
)}
</TableWithControlsLayout>
);
};
62 changes: 62 additions & 0 deletions src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<React.Fragment>
{error ? <ResponseError error={parseQueryErrorToString(error)} /> : null}
<TableWithControlsLayout.Table loading={isFetching && currentData === undefined}>
<ResizeableDataTable
emptyDataMessage={i18n('no-data')}
columnsWidthLSKey={TOP_QUERIES_COLUMNS_WIDTH_LS_KEY}
columns={columns}
data={data || []}
settings={QUERY_TABLE_SETTINGS}
onRowClick={handleRowClick}
rowClassName={() => b('row')}
/>
</TableWithControlsLayout.Table>
</React.Fragment>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyValueRow> = {
name: TOP_QUERIES_COLUMNS_IDS.CPUTimeUs,
Expand Down Expand Up @@ -94,6 +97,26 @@ const durationColumn: Column<KeyValueRow> = {
width: 150,
};

const queryStartColumn: Column<KeyValueRow> = {
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<KeyValueRow> = {
name: 'ApplicationName',
get header() {
return i18n('col_app');
},
render: ({row}) => <div className={b('user-sid')}>{row.ApplicationName || '–'}</div>,
sortable: true,
};

export const TOP_QUERIES_COLUMNS = [
cpuTimeUsColumn,
queryTextColumn,
Expand All @@ -109,3 +132,10 @@ export const TENANT_OVERVIEW_TOP_QUERUES_COLUMNS = [
oneLineQueryTextColumn,
cpuTimeUsColumn,
];

export const RUNNING_QUERIES_COLUMNS = [
userSIDColumn,
queryStartColumn,
queryTextColumn,
applicationColumn,
];
8 changes: 7 additions & 1 deletion src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 1 addition & 2 deletions src/containers/Tenant/Diagnostics/TopQueries/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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});
4 changes: 0 additions & 4 deletions src/containers/Tenant/Diagnostics/TopQueries/i18n/ru.json

This file was deleted.

Loading

0 comments on commit acd4a1e

Please sign in to comment.