diff --git a/src/components/ConnectToDB/ConnectToDBDialog.tsx b/src/components/ConnectToDB/ConnectToDBDialog.tsx index fe0a115678..e6636974ee 100644 --- a/src/components/ConnectToDB/ConnectToDBDialog.tsx +++ b/src/components/ConnectToDB/ConnectToDBDialog.tsx @@ -56,7 +56,7 @@ function ConnectToDBDialog({open, onClose, database, endpoint}: ConnectToDBDialo language={activeTab} text={snippet} transparentBackground={false} - withCopy + withClipboardButton={{alwaysVisible: true}} /> {docsLink ? ( diff --git a/src/components/DateRange/DateRange.scss b/src/components/DateRange/DateRange.scss index 0344b04273..5e50fddff5 100644 --- a/src/components/DateRange/DateRange.scss +++ b/src/components/DateRange/DateRange.scss @@ -1,7 +1,7 @@ .date-range { &__range-input { &_s { - width: 200px; + width: 130px; } &_m { diff --git a/src/components/DateRange/DateRange.tsx b/src/components/DateRange/DateRange.tsx index 14f1ef9cc3..a254ed1be9 100644 --- a/src/components/DateRange/DateRange.tsx +++ b/src/components/DateRange/DateRange.tsx @@ -1,6 +1,9 @@ import React from 'react'; -import type {RelativeRangeDatePickerProps} from '@gravity-ui/date-components'; +import type { + RelativeRangeDatePickerProps, + RelativeRangeDatePickerValue, +} from '@gravity-ui/date-components'; import {RelativeRangeDatePicker} from '@gravity-ui/date-components'; import {cn} from '../../utils/cn'; @@ -19,23 +22,13 @@ export interface DateRangeValues { to?: string; } -const DEFAULT_VALUE = { - start: { - value: 'now-1h', - type: 'relative', - }, - end: { - value: 'now', - type: 'relative', - }, -} as const; - interface DateRangeProps extends DateRangeValues { className?: string; + defaultValue?: RelativeRangeDatePickerValue; onChange?: (value: DateRangeValues) => void; } -export const DateRange = ({from, to, className, onChange}: DateRangeProps) => { +export const DateRange = ({from, to, className, defaultValue, onChange}: DateRangeProps) => { const handleUpdate = React.useCallback>( (pickerValue) => onChange?.(toDateRangeValues(pickerValue)), [onChange], @@ -50,13 +43,14 @@ export const DateRange = ({from, to, className, onChange}: DateRangeProps) => { // eslint-disable-next-line new-cap const timeZoneString = Intl.DateTimeFormat().resolvedOptions().timeZone; + const currentValue = value || defaultValue; return (
extends Omit, 'theme' | 'onResize'> { columnsWidthLSKey?: string; wrapperClassName?: string; + loading?: boolean; } export function ResizeableDataTable({ @@ -18,11 +20,20 @@ export function ResizeableDataTable({ columns, settings, wrapperClassName, + loading, ...props }: ResizeableDataTableProps) { const [tableColumnsWidth, setTableColumnsWidth] = useTableResize(columnsWidthLSKey); - const updatedColumns = updateColumnsWidth(columns, tableColumnsWidth); + // If loading is true, override the render method of each column to display a Skeleton + const processedColumns = loading + ? columns.map((column: Column) => ({ + ...column, + render: () => , + })) + : columns; + + const updatedColumns = updateColumnsWidth(processedColumns, tableColumnsWidth); const newSettings: Settings = { ...settings, diff --git a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss index 55a027d140..b549c666dc 100644 --- a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss +++ b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss @@ -16,5 +16,18 @@ position: absolute; top: 13px; right: 14px; + + pointer-events: all; + + opacity: 0; + + &_visible { + opacity: 1; + } + } + + .data-table__row:hover &__copy, + .ydb-paginated-table__row:hover &__copy { + opacity: 1; } } diff --git a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx index 8021ba93c9..b92fd2279e 100644 --- a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx +++ b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx @@ -23,12 +23,20 @@ async function registerLanguage(lang: Language) { } } +interface ClipboardButtonOptions { + alwaysVisible?: boolean; + copyText?: string; + withLabel?: boolean; +} + +export type WithClipboardButtonProp = ClipboardButtonOptions | boolean; + type YDBSyntaxHighlighterProps = { text: string; language: Language; className?: string; transparentBackground?: boolean; - withCopy?: boolean; + withClipboardButton?: WithClipboardButtonProp; }; export function YDBSyntaxHighlighter({ @@ -36,7 +44,7 @@ export function YDBSyntaxHighlighter({ language, className, transparentBackground = true, - withCopy, + withClipboardButton, }: YDBSyntaxHighlighterProps) { const [highlighterKey, setHighlighterKey] = React.useState(''); @@ -51,16 +59,27 @@ export function YDBSyntaxHighlighter({ }, [language]); const renderCopyButton = () => { - if (withCopy) { + if (withClipboardButton) { return ( -
+
e.stopPropagation()}> - {i18n('copy')} + {typeof withClipboardButton === 'object' && + withClipboardButton.withLabel === false + ? null + : i18n('copy')}
); diff --git a/src/components/TruncatedQuery/TruncatedQuery.tsx b/src/components/TruncatedQuery/TruncatedQuery.tsx index 952c2434a1..cf2bc61885 100644 --- a/src/components/TruncatedQuery/TruncatedQuery.tsx +++ b/src/components/TruncatedQuery/TruncatedQuery.tsx @@ -10,9 +10,16 @@ const b = cn('kv-truncated-query'); interface TruncatedQueryProps { value?: string; maxQueryHeight?: number; + hasClipboardButton?: boolean; + clipboardButtonAlwaysVisible?: boolean; } -export const TruncatedQuery = ({value = '', maxQueryHeight = 6}: TruncatedQueryProps) => { +export const TruncatedQuery = ({ + value = '', + maxQueryHeight = 6, + hasClipboardButton, + clipboardButtonAlwaysVisible, +}: TruncatedQueryProps) => { const lines = value.split('\n'); const truncated = lines.length > maxQueryHeight; @@ -22,10 +29,37 @@ export const TruncatedQuery = ({value = '', maxQueryHeight = 6}: TruncatedQueryP '\n...\nThe request was truncated. Click on the line to show the full query on the query tab'; return ( - + {message} ); } - return ; + return ( + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index 30c4c06362..afa9f2b982 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -15,7 +15,10 @@ import { TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID, } from '../../../../../store/reducers/tenant/constants'; -import {TENANT_OVERVIEW_TABLES_SETTINGS} from '../../../../../utils/constants'; +import { + TENANT_OVERVIEW_TABLES_LIMIT, + TENANT_OVERVIEW_TABLES_SETTINGS, +} from '../../../../../utils/constants'; import {useAutoRefreshInterval, useTypedDispatch} from '../../../../../utils/hooks'; import {useChangeInputWithConfirmation} from '../../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; import {parseQueryErrorToString} from '../../../../../utils/query'; @@ -45,7 +48,7 @@ export function TopQueries({tenantName}: TopQueriesProps) { }, []); const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( - {database: tenantName}, + {database: tenantName, timeFrame: 'hour', limit: TENANT_OVERVIEW_TABLES_LIMIT}, {pollingInterval: autoRefreshInterval}, ); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 1d0e011c98..e63a8dbb0e 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -1,67 +1,112 @@ import React from 'react'; +import type {Column} from '@gravity-ui/react-data-table'; +import {TableColumnSetup} from '@gravity-ui/uikit'; + import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {Search} from '../../../../components/Search'; 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 {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'; +import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; import {getRunningQueriesColumns} from './columns/columns'; -import {RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY} from './columns/constants'; +import { + DEFAULT_RUNNING_QUERIES_COLUMNS, + QUERIES_COLUMNS_TITLES, + REQUIRED_RUNNING_QUERIES_COLUMNS, + RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY, + RUNNING_QUERIES_SELECTED_COLUMNS_LS_KEY, +} from './columns/constants'; import i18n from './i18n'; import {TOP_QUERIES_TABLE_SETTINGS, useRunningQueriesSort} from './utils'; -interface Props { - database: string; +const b = cn('kv-top-queries'); + +interface RunningQueriesDataProps { + tenantName: string; + renderQueryModeControl: () => React.ReactNode; onRowClick: (query: string) => void; - rowClassName: string; + handleTextSearchUpdate: (text: string) => void; } -export const RunningQueriesData = ({database, onRowClick, rowClassName}: Props) => { +export const RunningQueriesData = ({ + tenantName, + renderQueryModeControl, + onRowClick, + handleTextSearchUpdate, +}: RunningQueriesDataProps) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); - const {tableSort, handleTableSort, backendSort} = useRunningQueriesSort(); + // Get columns for running queries + const columns: Column[] = React.useMemo(() => { + return getRunningQueriesColumns(); + }, []); - const {currentData, isFetching, error} = topQueriesApi.useGetRunningQueriesQuery( - { - database, - filters, - sortOrder: backendSort, - }, - {pollingInterval: autoRefreshInterval}, + // Use selected columns hook + const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( + columns, + RUNNING_QUERIES_SELECTED_COLUMNS_LS_KEY, + QUERIES_COLUMNS_TITLES, + DEFAULT_RUNNING_QUERIES_COLUMNS, + REQUIRED_RUNNING_QUERIES_COLUMNS, ); - const loading = isFetching && currentData === undefined; - - const data = currentData?.resultSets?.[0].result || []; + const {tableSort, handleTableSort, backendSort} = useRunningQueriesSort(); - const columns = React.useMemo(() => { - return getRunningQueriesColumns(); - }, []); + const {currentData, data, isFetching, isLoading, error} = + topQueriesApi.useGetRunningQueriesQuery( + { + database: tenantName, + filters, + sortOrder: backendSort, + }, + {pollingInterval: autoRefreshInterval}, + ); const handleRowClick = (row: KeyValueRow) => { return onRowClick(row.QueryText as string); }; return ( - + + + {renderQueryModeControl()} + + + + {error ? : null} - + rowClassName} + rowClassName={() => b('row')} sortOrder={tableSort} onSort={handleTableSort} /> - + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index 499a0d8cfb..1f430c4737 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -7,30 +7,26 @@ import {StringParam, useQueryParam} from 'use-query-params'; import {z} from 'zod'; import type {DateRangeValues} from '../../../../components/DateRange'; -import {DateRange} from '../../../../components/DateRange'; -import {Search} from '../../../../components/Search'; -import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {parseQuery} from '../../../../routes'; import {setTopQueriesFilters} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; +import type {TimeFrame} from '../../../../store/reducers/executeTopQueries/types'; import {changeUserInput, setIsDirty} from '../../../../store/reducers/query/query'; import { TENANT_PAGE, TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID, } from '../../../../store/reducers/tenant/constants'; -import {cn} from '../../../../utils/cn'; -import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useTypedDispatch} from '../../../../utils/hooks'; import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; import {RunningQueriesData} from './RunningQueriesData'; import {TopQueriesData} from './TopQueriesData'; +import {TimeFrameIds} from './constants'; import i18n from './i18n'; import './TopQueries.scss'; -const b = cn('kv-top-queries'); - const QueryModeIds = { top: 'top', running: 'running', @@ -52,6 +48,7 @@ const QUERY_MODE_OPTIONS: RadioButtonOption[] = [ ]; const queryModeSchema = z.nativeEnum(QueryModeIds).catch(QueryModeIds.top); +const timeFrameSchema = z.nativeEnum(TimeFrameIds).catch(TimeFrameIds.hour); interface TopQueriesProps { tenantName: string; @@ -62,13 +59,13 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { const location = useLocation(); const history = useHistory(); const [_queryMode = QueryModeIds.top, setQueryMode] = useQueryParam('queryMode', StringParam); + const [_timeFrame = TimeFrameIds.hour, setTimeFrame] = useQueryParam('timeFrame', StringParam); const queryMode = queryModeSchema.parse(_queryMode); + const timeFrame = timeFrameSchema.parse(_timeFrame); const isTopQueries = queryMode === QueryModeIds.top; - const filters = useTypedSelector((state) => state.executeTopQueries); - const applyRowClick = React.useCallback( (input: string) => { dispatch(changeUserInput({input})); @@ -93,35 +90,36 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { dispatch(setTopQueriesFilters({text})); }; + const handleTimeFrameChange = (value: string[]) => { + setTimeFrame(value[0] as TimeFrame, 'replaceIn'); + }; + const handleDateRangeChange = (value: DateRangeValues) => { dispatch(setTopQueriesFilters(value)); }; - const DataComponent = isTopQueries ? TopQueriesData : RunningQueriesData; - - return ( - - - - - {isTopQueries ? ( - - ) : null} - - - + const renderQueryModeControl = React.useCallback(() => { + return ( + + ); + }, [queryMode, setQueryMode]); + + return isTopQueries ? ( + + ) : ( + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index d447fbef81..debd299a6a 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -1,66 +1,133 @@ import React from 'react'; +import type {Column} from '@gravity-ui/react-data-table'; +import {Select, TableColumnSetup} from '@gravity-ui/uikit'; + +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 {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; +import type {TimeFrame} from '../../../../store/reducers/executeTopQueries/types'; import type {KeyValueRow} from '../../../../types/api/query'; +import {cn} from '../../../../utils/cn'; import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'; +import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; import {getTopQueriesColumns} from './columns/columns'; -import {TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} from './columns/constants'; +import { + DEFAULT_TOP_QUERIES_COLUMNS, + QUERIES_COLUMNS_TITLES, + REQUIRED_TOP_QUERIES_COLUMNS, + TOP_QUERIES_COLUMNS_WIDTH_LS_KEY, + TOP_QUERIES_SELECTED_COLUMNS_LS_KEY, +} from './columns/constants'; +import {DEFAULT_TIME_FILTER_VALUE, TIME_FRAME_OPTIONS} from './constants'; import i18n from './i18n'; import {TOP_QUERIES_TABLE_SETTINGS, useTopQueriesSort} from './utils'; -interface Props { - database: string; +const b = cn('kv-top-queries'); + +interface TopQueriesDataProps { + tenantName: string; + timeFrame: TimeFrame; + renderQueryModeControl: () => React.ReactNode; onRowClick: (query: string) => void; - rowClassName: string; + handleTimeFrameChange: (value: string[]) => void; + handleDateRangeChange: (value: DateRangeValues) => void; + handleTextSearchUpdate: (text: string) => void; } -export const TopQueriesData = ({database, onRowClick, rowClassName}: Props) => { +export const TopQueriesData = ({ + tenantName, + timeFrame, + renderQueryModeControl, + onRowClick, + handleTimeFrameChange, + handleDateRangeChange, + handleTextSearchUpdate, +}: TopQueriesDataProps) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); + // Get columns for top queries + const columns: Column[] = React.useMemo(() => { + return getTopQueriesColumns(); + }, []); + + // Use selected columns hook + const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( + columns, + TOP_QUERIES_SELECTED_COLUMNS_LS_KEY, + QUERIES_COLUMNS_TITLES, + DEFAULT_TOP_QUERIES_COLUMNS, + REQUIRED_TOP_QUERIES_COLUMNS, + ); + const {tableSort, handleTableSort, backendSort} = useTopQueriesSort(); - const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( + const {currentData, data, isFetching, isLoading, error} = topQueriesApi.useGetTopQueriesQuery( { - database, + database: tenantName, filters, sortOrder: backendSort, + timeFrame, }, {pollingInterval: autoRefreshInterval}, ); - const loading = isFetching && currentData === undefined; - - const data = currentData?.resultSets?.[0]?.result || []; - - const columns = React.useMemo(() => { - return getTopQueriesColumns(); - }, []); const handleRowClick = (row: KeyValueRow) => { return onRowClick(row.QueryText as string); }; return ( - + + + {renderQueryModeControl()} +