-
Notifications
You must be signed in to change notification settings - Fork 17
feat: show streaming query stats in Info tab #3060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
6a92b5a
ace6893
9ce4a5c
ccbc5b2
c6912ad
ab27dde
4fb7854
97f455c
fca7538
93a7f11
c5117d4
ad41dd7
9aadde9
82c0102
66e7175
88915a2
f654979
f0d64cf
aa621f2
46b85da
48967af
a06dcca
62a0382
37517ac
8536622
06121ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| 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; | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } catch { | ||
| errorData = errorRaw; | ||
| } | ||
| } else if (errorRaw) { | ||
| errorData = errorRaw as ErrorResponse; | ||
|
||
| } | ||
|
|
||
| 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:", | ||
|
||
| "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'; |
| 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'; | ||
|
|
||
|
|
@@ -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({ | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| data, | ||
| hideSeverity, | ||
| detailsMode = ISSUES_VIEW_MODE.INLINE, | ||
|
||
| }: 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, { | ||
|
||
| 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; | ||
|
||
| 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> | ||
|
||
| <IssueSeverity severity={severity} />{' '} | ||
| </React.Fragment> | ||
| )} | ||
| <span className={blockWrapper('error-message-text')}>{message}</span> | ||
|
|
||
| {hasIssues && ( | ||
| <Button view="normal" size="s" onClick={onClick}> | ||
| {buttonLabel} | ||
| </Button> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
@@ -88,7 +151,7 @@ export function Issues({issues, hideSeverity}: IssuesProps) { | |
| key={index} | ||
| hideSeverity={hideSeverity} | ||
| issue={issue} | ||
| expanded={issue === mostSevereIssue} | ||
| expanded={index === mostSevereIssue} | ||
| /> | ||
| ))} | ||
| </div> | ||
|
|
@@ -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 : []; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "action.show-details": "Show details", | ||
| "action.hide-details": "Hide details" | ||
| } |
| 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}); |
There was a problem hiding this comment.
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?