Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a92b5a
feat: show streaming query stats in Info tab
DaryaVorontsova Nov 10, 2025
ace6893
Merge branch 'main' into feat/streaming-query-info
DaryaVorontsova Nov 10, 2025
9ce4a5c
Add error display
DaryaVorontsova Nov 10, 2025
ccbc5b2
Fix i18n
DaryaVorontsova Nov 10, 2025
c6912ad
Merge branch 'main' into feat/streaming-query-info
DaryaVorontsova Nov 11, 2025
ab27dde
Fix modal
DaryaVorontsova Nov 11, 2025
4fb7854
Fix error displaying
DaryaVorontsova Nov 11, 2025
97f455c
Merge branch 'main' into feat/streaming-query-info
DaryaVorontsova Nov 11, 2025
fca7538
Fix styles
DaryaVorontsova Nov 12, 2025
93a7f11
Fix gap
DaryaVorontsova Nov 12, 2025
c5117d4
Merge branch 'main' into feat/streaming-query-info
DaryaVorontsova Nov 12, 2025
ad41dd7
Fix issues bug
DaryaVorontsova Nov 12, 2025
9aadde9
rollback package-lock.json to base branch state
DaryaVorontsova Nov 12, 2025
82c0102
Merge branch 'main' into feat/streaming-query-info
DaryaVorontsova Nov 12, 2025
66e7175
Fix bugs
DaryaVorontsova Nov 13, 2025
88915a2
Fix bugs
DaryaVorontsova Nov 13, 2025
f654979
Merge branch 'main' into feat/streaming-query-info
DaryaVorontsova Nov 13, 2025
f0d64cf
Fix error displaying
DaryaVorontsova Nov 11, 2025
aa621f2
Fix issues bug
DaryaVorontsova Nov 12, 2025
46b85da
rollback package-lock.json to base branch state
DaryaVorontsova Nov 12, 2025
48967af
Fix bugs
DaryaVorontsova Nov 13, 2025
a06dcca
Fix query text displaying
DaryaVorontsova Nov 13, 2025
62a0382
Fix i18n
DaryaVorontsova Nov 13, 2025
37517ac
Fix i18n
DaryaVorontsova Nov 13, 2025
8536622
Fix bugs
DaryaVorontsova Nov 13, 2025
06121ac
Fix i18n
DaryaVorontsova Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/components/ConfirmationDialog/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface CommonDialogProps {
caption?: string;
message?: React.ReactNode;
body?: React.ReactNode;
size?: 's' | 'm' | 'l';

progress?: boolean;
textButtonCancel?: string;
Expand All @@ -39,6 +40,7 @@ export const CONFIRMATION_DIALOG = 'confirmation-dialog';
function ConfirmationDialog({
caption = '',
children,
size = 's',
onConfirm,
onClose,
progress,
Expand All @@ -52,7 +54,7 @@ function ConfirmationDialog({
return (
<Dialog
className={block(null, className)}
size="s"
size={size}
onClose={onClose}
disableOutsideClick
open={open}
Expand Down
5 changes: 4 additions & 1 deletion src/containers/Tenant/Diagnostics/Overview/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {ViewInfo} from '../../Info/View/View';

import {AsyncReplicationInfo} from './AsyncReplicationInfo';
import {ChangefeedInfo} from './ChangefeedInfo';
import {StreamingQueryInfo} from './StreamingQueryInfo';
import {TableInfo} from './TableInfo';
import {TopicInfo} from './TopicInfo';
import {TransferInfo} from './TransferInfo';
Expand Down Expand Up @@ -77,7 +78,9 @@ function Overview({type, path, database, databaseFullPath}: OverviewProps) {
data={data}
/>
),
[EPathType.EPathTypeStreamingQuery]: undefined,
[EPathType.EPathTypeStreamingQuery]: () => (
<StreamingQueryInfo data={data} path={path} database={database} />
),
};

return (type && pathTypeToComponent[type]?.()) || <TableInfo data={data} type={type} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';

import {Label} from '@gravity-ui/uikit';

import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
import {YDBDefinitionList} from '../../../../../components/YDBDefinitionList/YDBDefinitionList';
import type {YDBDefinitionListItem} from '../../../../../components/YDBDefinitionList/YDBDefinitionList';
import {streamingQueriesApi} from '../../../../../store/reducers/streamingQuery/streamingQuery';
import type {ErrorResponse} from '../../../../../types/api/query';
import type {TEvDescribeSchemeResult} from '../../../../../types/api/schema';
import type {IQueryResult} from '../../../../../types/store/query';
import {getStringifiedData} from '../../../../../utils/dataFormatters/dataFormatters';
import {ResultIssues} from '../../../Query/Issues/Issues';
import {ISSUES_VIEW_MODE} from '../../../Query/Issues/models';
import {getEntityName} from '../../../utils';

import i18n from './i18n';

interface StreamingQueryProps {
data?: TEvDescribeSchemeResult;
database: string;
path: string;
}

/** Displays overview for StreamingQuery EPathType */
export function StreamingQueryInfo({data, database, path}: StreamingQueryProps) {
const entityName = getEntityName(data?.PathDescription);

if (!data) {
return (
<div className="error">
{i18n('fallback_no-data')} {entityName}
</div>
);
}

const {data: sysData} = streamingQueriesApi.useGetStreamingQueryInfoQuery(
{database, path},
{skip: !database || !path},
);

const items = prepareStreamingQueryItems(sysData);

return <YDBDefinitionList title={entityName} items={items} />;
}

const STATE_THEME_MAP: Record<string, React.ComponentProps<typeof Label>['theme']> = {
CREATING: 'info',
CREATED: 'normal',
STARTING: 'info',
RUNNING: 'success',
STOPPING: 'info',
STOPPED: 'normal',
COMPLETED: 'success',
SUSPENDED: 'warning',
FAILED: 'danger',
};

function renderStateLabel(state?: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you use render function here, not a component?

if (!state) {
return null;
}

const theme = STATE_THEME_MAP[state] ?? 'normal';

return <Label theme={theme}>{state}</Label>;
}

function prepareStreamingQueryItems(sysData?: IQueryResult): YDBDefinitionListItem[] {
if (!sysData) {
return [];
}

const info: YDBDefinitionListItem[] = [];
const state = getStringifiedData(sysData.resultSets?.[0]?.result?.[0]?.State);

const queryText = getStringifiedData(sysData.resultSets?.[0]?.result?.[0]?.Text);
const normalizedQueryText = typeof queryText === 'string' ? queryText.trim() : '';

const errorRaw = sysData.resultSets?.[0]?.result?.[0]?.Error;

// We need custom error check, because error type can be non-standard
let errorData: ErrorResponse | string | undefined;
if (typeof errorRaw === 'string') {
try {
errorData = JSON.parse(errorRaw) as ErrorResponse;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after parsing, let's check that parsed error is of type ErrorResponse (it seems type guard is needed)

} catch {
errorData = errorRaw;
}
} else if (errorRaw) {
errorData = errorRaw as ErrorResponse;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. Let's try not to use type casts, if it is possible.

}

info.push({
name: i18n('query.state-field'),
content: renderStateLabel(state),
});

if (errorData && Object.keys(errorData).length > 0) {
info.push({
name: i18n('query.error-field'),
content: <ResultIssues data={errorData} detailsMode={ISSUES_VIEW_MODE.MODAL} />,
});
}

info.push({
name: i18n('query.text-field'),
copyText: normalizedQueryText,
content: normalizedQueryText ? (
<YDBSyntaxHighlighter language="yql" text={normalizedQueryText} />
) : null,
});

return info;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"fallback_no-data": "No data for entity:",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets use rules of naming https://nda.ya.ru/t/mrGB97gd7MqSJh

"query.state-field": "State",
"query.error-field": "Error",
"query.text-field": "Text"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {registerKeysets} from '../../../../../../utils/i18n';

import en from './en.json';

const COMPONENT = 'ydb-diagnostics-streaming-query-info';

export default registerKeysets(COMPONENT, {en});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './StreamingQueryInfo';
158 changes: 121 additions & 37 deletions src/containers/Tenant/Query/Issues/Issues.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from 'react';

import NiceModal from '@ebay/nice-modal-react';
import {
CircleExclamationFill,
CircleInfoFill,
CircleXmarkFill,
TriangleExclamationFill,
} from '@gravity-ui/icons';
import type {IconData} from '@gravity-ui/uikit';
import {ArrowToggle, Button, Icon, Link} from '@gravity-ui/uikit';
import {ArrowToggle, Button, Flex, Icon, Link} from '@gravity-ui/uikit';

import {CONFIRMATION_DIALOG} from '../../../../components/ConfirmationDialog/ConfirmationDialog';
import ShortyString from '../../../../components/ShortyString/ShortyString';
import type {ErrorResponse, IssueMessage} from '../../../../types/api/query';
import {cn} from '../../../../utils/cn';
import {isNumeric} from '../../../../utils/utils';

import type {SEVERITY} from './models';
import {getSeverity} from './models';
import i18n from './i18n';
import type {IssuesViewMode, SEVERITY} from './models';
import {ISSUES_VIEW_MODE, getSeverity} from './models';

import './Issues.scss';

Expand All @@ -26,48 +29,108 @@ const blockIssue = cn('kv-issue');
interface ResultIssuesProps {
data: ErrorResponse | string;
hideSeverity?: boolean;

detailsMode?: IssuesViewMode;
}

export function ResultIssues({data, hideSeverity}: ResultIssuesProps) {
const [showIssues, setShowIssues] = React.useState(false);
export function ResultIssues({
data,
hideSeverity,
detailsMode = ISSUES_VIEW_MODE.INLINE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I would not add this property. Instead, I’d recommend splitting the component into two separate components. As it stands, the current implementation violates the single responsibility principle, which makes the code harder to read and maintain.

}: ResultIssuesProps) {
const roots = normalizeRoots(data);

const issues = typeof data === 'string' ? undefined : data?.issues;
const hasIssues = Array.isArray(issues) && issues.length > 0;
const [expanded, setExpanded] = React.useState<Record<number, boolean>>({});

const renderTitle = () => {
let content;
if (typeof data === 'string') {
content = data;
} else {
const severity = getSeverity(data?.error?.severity);
content = (
<React.Fragment>
{hideSeverity ? null : (
<React.Fragment>
<IssueSeverity severity={severity} />{' '}
</React.Fragment>
)}
<span className={blockWrapper('error-message-text')}>
{data?.error?.message}
</span>
</React.Fragment>
);
}
const onToggleInline = (idx: number) => setExpanded((p) => ({...p, [idx]: !p[idx]}));

return content;
const onOpenModal = (issues?: IssueMessage[]) => {
NiceModal.show(CONFIRMATION_DIALOG, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't notice this first time, sorry, but we shouldn't use ConfirmationDialog here, cause it's not about confirmations.

size: 'm',
children: <Issues issues={issues ?? []} />,
});
};

if (typeof data === 'string') {
return <div className={blockWrapper('error-message')}>{data}</div>;
}

return (
<div className={blockWrapper()}>
<div className={blockWrapper('error-message')}>
{renderTitle()}
{hasIssues && (
<Button view="normal" onClick={() => setShowIssues(!showIssues)}>
{showIssues ? 'Hide details' : 'Show details'}
</Button>
)}
</div>
{hasIssues && showIssues && <Issues hideSeverity={hideSeverity} issues={issues} />}
<Flex direction="column">
{roots.map((root, idx) => {
const hasIssues = Array.isArray(root.issues) && root.issues.length > 0;

return (
<React.Fragment key={idx}>
<ErrorPreviewItem
severity={getSeverity(root.severity)}
message={root.message || ''}
hideSeverity={hideSeverity}
mode={detailsMode}
hasIssues={hasIssues}
expanded={expanded[idx]}
onClick={
detailsMode === ISSUES_VIEW_MODE.MODAL
? () => onOpenModal(root.issues)
: () => onToggleInline(idx)
}
/>
{detailsMode === ISSUES_VIEW_MODE.INLINE &&
expanded[idx] &&
hasIssues && (
<Issues hideSeverity={hideSeverity} issues={root.issues} />
)}
</React.Fragment>
);
})}
</Flex>
</div>
);
}

interface ErrorPreviewItemProps {
severity: SEVERITY;
message?: string;
hideSeverity?: boolean;
mode: IssuesViewMode;
hasIssues?: boolean;
expanded?: boolean;
onClick: () => void;
}

export function ErrorPreviewItem({
severity,
message,
hideSeverity,
mode = ISSUES_VIEW_MODE.INLINE,
hasIssues,
expanded,
onClick,
}: ErrorPreviewItemProps) {
let buttonLabel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

const buttonLabel = expanded ? i18n('action.hide-details') : i18n('action.show-details')

if (mode === ISSUES_VIEW_MODE.MODAL) {
buttonLabel = i18n('action.show-details');
} else if (expanded) {
buttonLabel = i18n('action.hide-details');
} else {
buttonLabel = i18n('action.show-details');
}

return (
<div className={blockWrapper('error-message')}>
{hideSeverity ? null : (
<React.Fragment>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Fragment is not needed here if we add gap for error-message css-class?

<IssueSeverity severity={severity} />{' '}
</React.Fragment>
)}
<span className={blockWrapper('error-message-text')}>{message}</span>

{hasIssues && (
<Button view="normal" size="s" onClick={onClick}>
{buttonLabel}
</Button>
)}
</div>
);
}
Expand All @@ -88,7 +151,7 @@ export function Issues({issues, hideSeverity}: IssuesProps) {
key={index}
hideSeverity={hideSeverity}
issue={issue}
expanded={issue === mostSevereIssue}
expanded={index === mostSevereIssue}
/>
))}
</div>
Expand Down Expand Up @@ -228,3 +291,24 @@ function getIssuePosition(issue: IssueMessage): string {

return isNumeric(column) ? `${row}:${column}` : `line ${row}`;
}

function normalizeRoots(data: ErrorResponse | string): IssueMessage[] {
if (typeof data === 'string') {
return [];
}

if (data?.error?.message) {
return [
{
message: data.error.message,
severity: data.error.severity,
position: data.error.position,
end_position: data.error.end_position,
issue_code: data.error.issue_code,
issues: Array.isArray(data.issues) ? data.issues : [],
},
];
}

return Array.isArray(data.issues) ? data.issues : [];
}
4 changes: 4 additions & 0 deletions src/containers/Tenant/Query/Issues/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"action.show-details": "Show details",
"action.hide-details": "Hide details"
}
7 changes: 7 additions & 0 deletions src/containers/Tenant/Query/Issues/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {registerKeysets} from '../../../../../utils/i18n';

import en from './en.json';

const COMPONENT = 'issues';

export default registerKeysets(COMPONENT, {en});
Loading
Loading