-
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 5 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,144 @@ | ||
| import React from 'react'; | ||
|
|
||
| import NiceModal from '@ebay/nice-modal-react'; | ||
| import {Flex, Label} from '@gravity-ui/uikit'; | ||
|
|
||
| import {CONFIRMATION_DIALOG} from '../../../../../components/ConfirmationDialog/ConfirmationDialog'; | ||
| 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 {Issues, ResultIssues} from '../../../Query/Issues/Issues'; | ||
| 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('noData')} {entityName} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const {data: sysData} = streamingQueriesApi.useGetStreamingQueryInfoQuery( | ||
| {database, path}, | ||
| {skip: !database || !path}, | ||
| ); | ||
|
|
||
| const items = prepareStreamingQueryItems(sysData); | ||
|
|
||
| return ( | ||
| <Flex direction="column" gap="4"> | ||
| <YDBDefinitionList title={entityName} items={items} /> | ||
| </Flex> | ||
| ); | ||
| } | ||
|
|
||
| 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 = normalizeQueryText(queryText); | ||
|
|
||
| const errorRaw = sysData.resultSets?.[0]?.result?.[0]?.Error; | ||
|
|
||
| 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('state.label'), | ||
| content: renderStateLabel(state), | ||
| }); | ||
|
|
||
| if (errorData && Object.keys(errorData).length > 0) { | ||
| const issues = typeof errorData === 'string' ? undefined : errorData.issues; | ||
|
|
||
| info.push({ | ||
| name: i18n('state.error'), | ||
| content: ( | ||
| <ResultIssues | ||
| data={errorData} | ||
| titlePreviewMode="multi" | ||
| detailsMode="modal" | ||
| onOpenDetails={() => | ||
| NiceModal.show(CONFIRMATION_DIALOG, { | ||
| caption: i18n('state.error'), | ||
| children: <Issues issues={issues ?? []} />, | ||
| }) | ||
| } | ||
| /> | ||
| ), | ||
| }); | ||
| } | ||
|
|
||
| info.push({ | ||
| name: i18n('text.label'), | ||
| copyText: normalizedQueryText, | ||
| content: normalizedQueryText ? ( | ||
| <YDBSyntaxHighlighter language="yql" text={normalizedQueryText} /> | ||
| ) : null, | ||
| }); | ||
|
|
||
| return info; | ||
| } | ||
|
|
||
| function normalizeQueryText(text?: string) { | ||
| if (!text) { | ||
| return text; | ||
| } | ||
|
|
||
| let normalized = text.replace(/^\s*\n+/, ''); | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| normalized = normalized.replace(/\n+\s*$/, ''); | ||
|
|
||
| return normalized; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "noData": "No data for entity:", | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "state.label": "State", | ||
| "state.error": "Error", | ||
| "text.label": "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 |
|---|---|---|
|
|
@@ -26,48 +26,98 @@ const blockIssue = cn('kv-issue'); | |
| interface ResultIssuesProps { | ||
| data: ErrorResponse | string; | ||
| hideSeverity?: boolean; | ||
|
|
||
| // 'inline': Expands/collapses the full issues tree right below the preview. | ||
| // 'modal': Opens the full issues tree in an extra modal on "Show details". | ||
| detailsMode?: 'inline' | 'modal'; | ||
| onOpenDetails?: () => void; | ||
|
|
||
| // 'single': Show a single top line: "<Severity> Error + root error.message". | ||
| // 'multi': If there is no root error.message, preview can show all first-level issues. | ||
| // Otherwise falls back to 'single'. | ||
| titlePreviewMode?: 'single' | 'multi'; | ||
| } | ||
|
|
||
| export function ResultIssues({data, hideSeverity}: ResultIssuesProps) { | ||
| export function ResultIssues({ | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| data, | ||
| hideSeverity, | ||
| detailsMode = 'inline', | ||
| onOpenDetails, | ||
| titlePreviewMode = 'single', | ||
| }: ResultIssuesProps) { | ||
| const [showIssues, setShowIssues] = React.useState(false); | ||
|
|
||
| const issues = typeof data === 'string' ? undefined : data?.issues; | ||
| const hasIssues = Array.isArray(issues) && issues.length > 0; | ||
|
|
||
| const renderTitle = () => { | ||
| let content; | ||
| if (typeof data === 'string') { | ||
| content = data; | ||
| } else { | ||
| const severity = getSeverity(data?.error?.severity); | ||
| content = ( | ||
| const rootHasMessage = typeof data === 'string' ? undefined : data?.error?.message; | ||
|
|
||
| const isMultiApplicable = titlePreviewMode === 'multi' && hasIssues && !rootHasMessage; | ||
|
|
||
| const renderSinglePreviewLine = (severity: SEVERITY, message?: string) => ( | ||
| <React.Fragment> | ||
| {hideSeverity ? null : ( | ||
| <React.Fragment> | ||
|
||
| {hideSeverity ? null : ( | ||
| <React.Fragment> | ||
| <IssueSeverity severity={severity} />{' '} | ||
| </React.Fragment> | ||
| )} | ||
| <span className={blockWrapper('error-message-text')}> | ||
| {data?.error?.message} | ||
| </span> | ||
| <IssueSeverity severity={severity} />{' '} | ||
| </React.Fragment> | ||
| )} | ||
| <span className={blockWrapper('error-message-text')}>{message}</span> | ||
| </React.Fragment> | ||
| ); | ||
|
|
||
| const renderTitle = () => { | ||
| if (typeof data === 'string') { | ||
| return data; | ||
| } | ||
|
|
||
| if (isMultiApplicable) { | ||
| return ( | ||
| <div className={blockWrapper('error-list')}> | ||
| {issues.map((issue, idx) => { | ||
| const severity = getSeverity(issue.severity); | ||
| return ( | ||
| <div className={blockWrapper('error-list-item')} key={idx}> | ||
| {renderSinglePreviewLine(severity, issue.message)} | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const severity = getSeverity(data?.error?.severity); | ||
| return renderSinglePreviewLine(severity, data?.error?.message ?? ''); | ||
| }; | ||
|
|
||
| const renderDetailsControl = () => { | ||
| if (!hasIssues) { | ||
| return null; | ||
| } | ||
|
|
||
| if (detailsMode === 'modal') { | ||
| return ( | ||
| <Button view="normal" onClick={() => onOpenDetails?.()}> | ||
| Show details | ||
| </Button> | ||
| ); | ||
| } | ||
|
|
||
| return content; | ||
| return ( | ||
| <Button view="normal" onClick={() => setShowIssues(!showIssues)}> | ||
| {showIssues ? 'Hide details' : 'Show details'} | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </Button> | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className={blockWrapper()}> | ||
| <div className={blockWrapper('error-message')}> | ||
| <div className={blockWrapper('error-message', {column: isMultiApplicable})}> | ||
| {renderTitle()} | ||
| {hasIssues && ( | ||
| <Button view="normal" onClick={() => setShowIssues(!showIssues)}> | ||
| {showIssues ? 'Hide details' : 'Show details'} | ||
| </Button> | ||
| )} | ||
| {renderDetailsControl()} | ||
| </div> | ||
| {hasIssues && showIssues && <Issues hideSeverity={hideSeverity} issues={issues} />} | ||
| {detailsMode === 'inline' && hasIssues && showIssues && ( | ||
| <Issues hideSeverity={hideSeverity} issues={issues} /> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import {QUERY_TECHNICAL_MARK} from '../../../utils/constants'; | ||
| import {isQueryErrorResponse, parseQueryAPIResponse} from '../../../utils/query'; | ||
| import {api} from '../api'; | ||
|
|
||
| function getStreamingQueryInfoSQL(path: string) { | ||
| const safePath = path.replace(/'/g, "''"); | ||
| return `${QUERY_TECHNICAL_MARK} | ||
| SELECT | ||
| Status AS State, | ||
| Issues AS Error, | ||
| Text | ||
| FROM \`.sys/streaming_queries\` | ||
| WHERE Path = '${safePath}' | ||
| LIMIT 1`; | ||
| } | ||
|
|
||
| export const streamingQueriesApi = api.injectEndpoints({ | ||
| endpoints: (build) => ({ | ||
| getStreamingQueryInfo: build.query({ | ||
| queryFn: async ({database, path}: {database: string; path: string}, {signal}) => { | ||
| try { | ||
| const response = await window.api.viewer.sendQuery( | ||
| { | ||
| query: getStreamingQueryInfoSQL(path), | ||
| database, | ||
| action: 'execute-scan', | ||
| internal_call: true, | ||
| }, | ||
| {signal, withRetries: true}, | ||
| ); | ||
|
|
||
| if (isQueryErrorResponse(response)) { | ||
| return {error: response}; | ||
| } | ||
|
|
||
| const data = parseQueryAPIResponse(response); | ||
| return {data}; | ||
| } catch (error) { | ||
| return {error}; | ||
| } | ||
| }, | ||
| forceRefetch() { | ||
DaryaVorontsova marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return true; | ||
| }, | ||
| providesTags: ['All'], | ||
| }), | ||
| }), | ||
| overrideExisting: 'throw', | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.