From 415b9b0c9fb8b29cfaa66d72a5fc21f54972c204 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Mon, 28 Sep 2026 17:31:25 +0300 Subject: [PATCH 1/4] fix: align query execution panel with design --- .storybook/preview.tsx | 1 + .../QueryResultsTable/QueryResultsTable.scss | 7 +- .../NavigationMeta/NavigationMeta.scss | 36 ++-- src/modules/NavigationMeta/NavigationMeta.tsx | 15 +- src/modules/QueryProgress/QueryProgress.scss | 37 +--- src/modules/QueryProgress/QueryProgress.tsx | 29 ++- src/modules/QueryResults/QueryResults.scss | 6 + src/modules/QueryResults/QueryResults.tsx | 6 +- .../QueryStatistics/QueryStatistics.scss | 1 + .../internal/QueryStatisticsToolbar.scss | 9 +- .../internal/QueryStatisticsToolbar.tsx | 23 +- src/types/queryExecutionPanel.ts | 5 + .../QueryExecutionPanel.scss | 14 +- .../QueryExecutionPanel.tsx | 25 ++- src/widgets/QueryExecutionPanel/README.md | 7 + src/widgets/QueryExecutionPanel/i18n/en.json | 6 +- src/widgets/QueryExecutionPanel/i18n/ru.json | 6 +- .../internal/QueryExecutionState.scss | 6 + .../internal/QueryExecutionState.tsx | 35 +++ .../story/QueryExecutionPanel.stories.scss | 18 ++ .../story/QueryExecutionPanel.stories.tsx | 50 ++++- .../story/designReference.tsx | 199 ++++++++++++++++++ tests/components/QueryExecutionPanel.test.tsx | 34 ++- tests/components/QueryProgress.test.tsx | 1 + 24 files changed, 466 insertions(+), 110 deletions(-) create mode 100644 src/widgets/QueryExecutionPanel/internal/QueryExecutionState.scss create mode 100644 src/widgets/QueryExecutionPanel/internal/QueryExecutionState.tsx create mode 100644 src/widgets/QueryExecutionPanel/story/designReference.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f04dc2c..8965777 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,6 +2,7 @@ import type {Decorator, Preview} from '@storybook/react'; import {ThemeProvider, configure} from '@gravity-ui/uikit'; import React from 'react'; +import '@gravity-ui/uikit/styles/fonts.css'; import '@gravity-ui/uikit/styles/styles.css'; import '@gravity-ui/unipika/dist/unipika.css'; import '@gravity-ui/illustrations/styles/styles.scss'; diff --git a/src/components/QueryResultsTable/QueryResultsTable.scss b/src/components/QueryResultsTable/QueryResultsTable.scss index c1956ff..aae6f6a 100644 --- a/src/components/QueryResultsTable/QueryResultsTable.scss +++ b/src/components/QueryResultsTable/QueryResultsTable.scss @@ -28,9 +28,9 @@ &__cell { display: block; padding-right: var(--g-spacing-5); - font-family: var(--g-font-family-monospace); - font-size: var(--g-text-code-2-font-size); - line-height: var(--g-text-code-2-line-height); + font-family: var(--g-text-body-font-family); + font-size: var(--g-text-body-1-font-size); + line-height: var(--g-text-body-1-line-height); overflow-wrap: anywhere; &:hover .qp-query-results-table__cell-actions, @@ -40,6 +40,7 @@ .unipika { display: inline; + font: inherit; } } diff --git a/src/modules/NavigationMeta/NavigationMeta.scss b/src/modules/NavigationMeta/NavigationMeta.scss index 691fd8c..ae40ae3 100644 --- a/src/modules/NavigationMeta/NavigationMeta.scss +++ b/src/modules/NavigationMeta/NavigationMeta.scss @@ -1,25 +1,23 @@ .qp-navigation-meta { - &__error { - display: block; - } + &__error { + display: block; + } - &__group-title { - display: block; - margin-bottom: var(--g-spacing-2); - } + &__group-title { + display: block; + margin-bottom: var(--g-spacing-2); + } - &__group-body { - width: 100%; - display: grid; - grid-gap: 12px; - grid-template-columns: 128px 1fr; - } + &__group-body { + --g-definition-list-item-gap: var(--g-spacing-3); + overflow-wrap: anywhere; + } - &__skeleton-row { - height: 24px; - } + &__skeleton-row { + height: 24px; + } - &__empty { - height: 100%; - } + &__empty { + height: 100%; + } } diff --git a/src/modules/NavigationMeta/NavigationMeta.tsx b/src/modules/NavigationMeta/NavigationMeta.tsx index 2bb560e..0c1f16f 100644 --- a/src/modules/NavigationMeta/NavigationMeta.tsx +++ b/src/modules/NavigationMeta/NavigationMeta.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import {Flex, Text} from '@gravity-ui/uikit'; +import {DefinitionList, Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; import {EmptyContent} from '../../components/EmptyContent'; import {SkeletonRows} from '../../components/SkeletonRows'; @@ -65,14 +65,13 @@ export function NavigationMeta ) : null} -
- {group.items.map(({name, value}) => ( - <> - {name} -
{value}
- + + {group.items.map(({name, value}, itemIndex) => ( + + {value} + ))} -
+ ), )} diff --git a/src/modules/QueryProgress/QueryProgress.scss b/src/modules/QueryProgress/QueryProgress.scss index abc4588..41f318d 100644 --- a/src/modules/QueryProgress/QueryProgress.scss +++ b/src/modules/QueryProgress/QueryProgress.scss @@ -6,48 +6,13 @@ &__toolbar { flex-shrink: 0; flex-wrap: wrap; + padding: var(--g-spacing-2) var(--g-spacing-4); } &__statuses { margin-left: auto; } - &__status { - display: inline-flex; - align-items: center; - gap: var(--g-spacing-1); - padding: var(--g-spacing-1) var(--g-spacing-2); - border-radius: var(--g-border-radius-s); - color: var(--g-color-text-secondary); - background: var(--g-color-base-background); - - &_status_not-started { - color: var(--g-color-text-primary); - box-shadow: inset 0 0 0 1px var(--g-color-line-generic); - } - - &_status_waiting { - color: var(--g-color-text-misc-heavy); - background: var(--g-color-base-misc-light); - } - &_status_running { - color: var(--g-color-text-info); - background: var(--g-color-base-info-light); - } - &_status_completed { - color: var(--g-color-text-positive); - background: var(--g-color-base-positive-light); - } - &_status_failed { - color: var(--g-color-text-danger); - background: var(--g-color-base-danger-light); - } - &_status_aborted { - color: var(--g-color-text-complementary); - background: var(--g-color-base-generic); - } - } - &__content, &__view { flex: 1; diff --git a/src/modules/QueryProgress/QueryProgress.tsx b/src/modules/QueryProgress/QueryProgress.tsx index 7688e22..2e5618b 100644 --- a/src/modules/QueryProgress/QueryProgress.tsx +++ b/src/modules/QueryProgress/QueryProgress.tsx @@ -7,7 +7,8 @@ import { CirclePlay, CircleXmark, } from '@gravity-ui/icons'; -import {Flex, Icon, Loader, SegmentedRadioGroup} from '@gravity-ui/uikit'; +import {Flex, Icon, Label, Loader, SegmentedRadioGroup} from '@gravity-ui/uikit'; +import type {LabelProps} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; import type { @@ -46,6 +47,15 @@ const statusIcons = { aborted: CircleMinus, } satisfies Record; +const statusThemes: Record = { + 'not-started': 'clear', + waiting: 'normal', + running: 'info', + completed: 'success', + failed: 'danger', + aborted: 'normal', +}; + export function QueryProgress({ active = true, graphProps, @@ -85,7 +95,7 @@ export function QueryProgress({ ); return ( - + @@ -96,12 +106,17 @@ export function QueryProgress({ {view === 'graph' && ( - + {statuses.map((status) => ( - - - {i18n(`status_${status}`)}: {counts[status]} - + ))} )} diff --git a/src/modules/QueryResults/QueryResults.scss b/src/modules/QueryResults/QueryResults.scss index 68dbc33..1648ef4 100644 --- a/src/modules/QueryResults/QueryResults.scss +++ b/src/modules/QueryResults/QueryResults.scss @@ -3,6 +3,11 @@ &__toolbar { min-width: 0; + padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-4); + position: sticky; + top: 0; + z-index: 1; + background: var(--g-color-base-background); } &__views, @@ -21,5 +26,6 @@ &__content { min-width: 0; + padding-inline: var(--g-spacing-4); } } diff --git a/src/modules/QueryResults/QueryResults.tsx b/src/modules/QueryResults/QueryResults.tsx index 6c2d46b..46a928b 100644 --- a/src/modules/QueryResults/QueryResults.tsx +++ b/src/modules/QueryResults/QueryResults.tsx @@ -65,14 +65,14 @@ export function QueryResults>({ } return ( - + {title && {title}} - + diff --git a/src/modules/QueryStatistics/QueryStatistics.scss b/src/modules/QueryStatistics/QueryStatistics.scss index c6b7f40..5706a17 100644 --- a/src/modules/QueryStatistics/QueryStatistics.scss +++ b/src/modules/QueryStatistics/QueryStatistics.scss @@ -1,3 +1,4 @@ .qp-query-statistics { min-width: 0; + padding-inline: var(--g-spacing-4); } diff --git a/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.scss b/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.scss index ea822f1..545f57a 100644 --- a/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.scss +++ b/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.scss @@ -1 +1,8 @@ -.qp-query-statistics-toolbar { margin-block-end: var(--g-spacing-4); max-width: 34rem; } +.qp-query-statistics-toolbar { + padding-block: var(--g-spacing-2); + + &__search { + min-width: 0; + max-width: 320px; + } +} diff --git a/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.tsx b/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.tsx index 9e6eee5..7280f00 100644 --- a/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.tsx +++ b/src/modules/QueryStatistics/internal/QueryStatisticsToolbar.tsx @@ -1,9 +1,8 @@ import React from 'react'; import {ChevronsDown, ChevronsUp} from '@gravity-ui/icons'; -import {Button, Icon} from '@gravity-ui/uikit'; +import {Button, Flex, Icon, TextInput} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import {SearchWithButtons} from '../../../components/SearchWithButtons'; import i18n from '../i18n'; import './QueryStatisticsToolbar.scss'; @@ -29,6 +28,7 @@ export function QueryStatisticsToolbar({ } + + )} + + ); +} diff --git a/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.scss b/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.scss index 7c2fb1e..59e518b 100644 --- a/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.scss +++ b/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.scss @@ -2,6 +2,24 @@ height: 640px; min-width: 0; + &__reference { + height: 100%; + // Application branding is scoped to the reference story. Semantic info + // tokens supply the blue palette for both light and dark themes. + --g-color-base-brand: var(--g-color-base-info-heavy); + --g-color-base-brand-hover: var(--g-color-base-info-heavy-hover); + --g-color-base-selection: var(--g-color-base-info-medium); + --g-color-base-selection-hover: var(--g-color-base-info-medium-hover); + --g-color-line-brand: var(--g-color-base-info-heavy); + --g-color-text-brand: var(--g-color-text-info); + --g-color-text-brand-heavy: var(--g-color-text-info-heavy); + --g-color-text-brand-contrast: var(--g-color-text-light-primary); + --g-color-text-link: var(--g-color-text-info); + --g-color-text-link-hover: var(--g-color-text-info-heavy); + --g-color-text-link-visited: var(--g-color-text-misc); + --g-color-text-link-visited-hover: var(--g-color-text-misc-heavy); + } + &__layout { height: 100%; background: var(--g-color-base-background); diff --git a/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.tsx b/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.tsx index 32bfd41..f6e8f17 100644 --- a/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.tsx +++ b/src/widgets/QueryExecutionPanel/story/QueryExecutionPanel.stories.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import type {Meta, StoryObj} from '@storybook/react'; +import type {Meta as StoryMeta, StoryObj} from '@storybook/react'; import {Button, Flex, Text, TextInput} from '@gravity-ui/uikit'; import {QueryExecutionPanel} from '../QueryExecutionPanel'; import type {QueryExecutionTab} from '../../../types/queryExecutionPanel'; @@ -9,6 +9,7 @@ import {lineSeriesMap} from '../../DashboardCharts/story/mockData'; import {demoEdges, demoNodes} from '../../../components/QueryGraph/QueryGraph.stories'; import {createQueryTrackerPanel} from './queryTrackerAdapter'; import type {ExampleQuery} from './queryTrackerAdapter'; +import {designTabs} from './designReference'; import './QueryExecutionPanel.stories.scss'; const results = { @@ -58,11 +59,11 @@ const allTabs: QueryExecutionTab[] = [ renderContent: ({active}) => , }, ]; -const meta: Meta = { +const meta: StoryMeta = { title: 'Widgets/QueryExecutionPanel', component: QueryExecutionPanel, tags: ['autodocs'], - args: {tabs: allTabs, execution: {startedAt: '28 Sep 2026, 12:07:27', author: 'admin'}}, + args: {tabs: designTabs, execution: {startedAt: '11 Dec 2025, 12:07:27', author: 'admin'}}, decorators: [ (Story) => (
@@ -74,6 +75,9 @@ const meta: Meta = { export default meta; type Story = StoryObj; export const Default: Story = {}; +export const Statistics: Story = {args: {defaultActiveTab: 'statistics'}}; +export const Meta: Story = {args: {defaultActiveTab: 'meta'}}; +export const Info: Story = {args: {defaultActiveTab: 'info'}}; export const Progress: Story = {args: {defaultActiveTab: 'progress'}}; export const Charts: Story = {args: {defaultActiveTab: 'charts'}}; export const Narrow: Story = { @@ -87,12 +91,38 @@ export const Narrow: Story = { }; export const Empty: Story = {args: {tabs: []}}; export const EmptyInfo: Story = {args: {tabs: [{id: 'info', type: 'info'}]}}; -export const Loading: Story = { - args: { - tabs: [ - {id: 'result', type: 'result', props: {...results, rows: [], loading: true}}, - {...allTabs[2], disabled: true}, - ], +export const Loading: Story = {args: {loading: true}}; +export const LoadingError: Story = { + render: function LoadingErrorExample(args) { + const [error, setError] = useState(true); + return setError(false)} />; + }, +}; +export const DesignReference: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], + render: function DesignReferenceExample(args) { + const [expanded, setExpanded] = useState(false); + const [closed, setClosed] = useState(false); + return closed ? ( + + ) : ( +
+ setClosed(true)} + /> +
+ ); }, }; export const CustomOrder: Story = {args: {tabs: [allTabs[7], allTabs[1], allTabs[0], allTabs[5]]}}; @@ -130,6 +160,7 @@ export const DynamicMessages: Story = { }, }; export const Controlled: Story = { + args: {tabs: allTabs}, render: function ControlledExample(args) { const [activeTab, setActiveTab] = useState('result/0'); return ( @@ -181,6 +212,7 @@ export const QueryTrackerLifecycle: Story = { }, }; export const ApplicationLayout: Story = { + args: {tabs: allTabs}, render: function ApplicationLayoutExample(args) { const [expanded, setExpanded] = useState(false); const [closed, setClosed] = useState(false); diff --git a/src/widgets/QueryExecutionPanel/story/designReference.tsx b/src/widgets/QueryExecutionPanel/story/designReference.tsx new file mode 100644 index 0000000..a3abf40 --- /dev/null +++ b/src/widgets/QueryExecutionPanel/story/designReference.tsx @@ -0,0 +1,199 @@ +import React, {useState} from 'react'; +import {ArrowDownToLine, ArrowUpFromLine, CircleCheck, Gear} from '@gravity-ui/icons'; +import {Button, Flex, Icon, Label} from '@gravity-ui/uikit'; +import type {QueryExecutionTab} from '../../../types/queryExecutionPanel'; +import type {QueryResultsProps} from '../../../types/queryResults'; +import {DashboardCharts} from '../../DashboardCharts'; +import type {DashboardChartsProps} from '../../DashboardCharts/types'; +import {demoEdges, demoNodes} from '../../../components/QueryGraph/QueryGraph.stories'; + +const result: QueryResultsProps> = { + columns: [ + {name: 'age', type: ['DataType', 'Int32'], width: 56}, + {name: 'ip', type: ['DataType', 'Utf8'], width: 160}, + {name: 'last_time_on_site', type: ['DataType', 'Double'], width: 128}, + {name: 'last_url', type: ['DataType', 'Utf8'], width: 400}, + {name: 'last_visit_time', type: ['DataType', 'Int64'], width: 128}, + {name: 'name', type: ['DataType', 'Utf8'], width: 128}, + {name: 'region', type: ['DataType', 'Int32'], width: 128}, + {name: 'user_agent', type: ['DataType', 'Utf8'], width: 128}, + ], + rows: ['Anya', 'Petr', 'Masha', 'Alena', 'Irina', 'Anna', 'Ivan'].map((name, index) => ({ + age: String([15, 25, 17, 5, 23, 13, 33][index]), + ip: ['95.106.17.32', '88.78.248.151', '93.94.183.63'][index % 3], + last_time_on_site: String([15.5, 5, 10.5, 22.5, 15, 19.5, 15.5][index]), + last_url: + index === 6 + ? 'https://maps.yandex.ru/?ll=37.671587%2C55.867321&z=15&text=search&sspn=0.105829%2C0.026809' + : 'https://yandex.ru/', + last_visit_time: String(1447027200 - index * 604800), + name, + region: String([213, 225, 1, 225, 2, 21, 125][index]), + user_agent: '', + })), + actions: ( + + + + + + ), +}; + +const chartItems: NonNullable = [0, 1].map((index) => ({ + id: `chart-${index}`, + chartData: { + title: {text: 'Push Process Errors'}, + legend: {enabled: false}, + xAxis: {type: 'datetime'}, + yAxis: [{min: 0}], + series: { + data: [ + { + type: 'line', + seriesId: `errors-${index}`, + name: 'Errors', + data: [ + 130, 100, 155, 175, 160, 100, 165, 180, 150, 190, 170, 125, 155, 40, 145, + 155, + ].map((y, i) => ({ + x: Date.UTC(2025, 11, 11, 1) + i * 20 * 60 * 1000, + y: y * 1e6, + })), + }, + ], + }, + }, +})); + +function ReferenceCharts() { + const [items, setItems] = useState(chartItems); + return ( + { + const series = item.chartData.series.data[0]; + return [series.seriesId, series]; + }), + ), + }} + chartItems={items} + onItemsChange={setItems} + defaultLayout={chartItems.map(({id}, i) => ({i: id, x: i * 2, y: 0, w: 2, h: 4}))} + /> + ); +} + +export const designTabs: QueryExecutionTab[] = [ + {id: 'result', type: 'result', props: result}, + {id: 'progress', type: 'progress', props: {graphProps: {nodes: demoNodes, edges: demoEdges}}}, + { + id: 'info', + type: 'info', + props: { + root: { + id: 'root', + severity: 'error', + message: 'Missing value for parameter: $h', + code: 2, + attributes: {parameter: '$h'}, + children: [ + { + id: 'child', + severity: 'error', + message: 'Missing value for parameter: $h', + code: 2, + attributes: {parameter: '$h'}, + }, + ], + }, + }, + }, + { + id: 'statistics', + type: 'statistics', + props: { + data: [ + { + id: 'group-1', + name: 'Value', + children: [ + { + id: 'group-2', + name: 'Value', + children: [{id: 'metric', name: 'Value', values: {}}], + }, + ], + }, + { + id: 'group-3', + name: 'Value', + children: [{id: 'metric-2', name: 'Value', values: {}}], + }, + { + id: 'group-4', + name: 'Value', + children: [{id: 'metric-3', name: 'Value', values: {}}], + }, + ], + defaultExpandedIds: ['group-1', 'group-2'], + }, + }, + { + id: 'meta', + type: 'meta', + props: { + data: { + groups: [ + { + items: [ + {name: 'ID', value: '0yui78900ghjkl'}, + { + name: 'Status', + value: ( + + ), + }, + {name: 'Execution mode', value: 'run'}, + {name: 'Created', value: '27.12.2024 18:56:32'}, + {name: 'Modified', value: '27.12.2024 18:56:32'}, + {name: 'Worker ID', value: '89d97cc9-43ba6556-9bb1c687-62567115'}, + {name: 'Worker PID', value: '587651'}, + { + name: 'Worker host', + value: 'yt-query-tracker-production-1.sas.yp-c.yandex.net:9028', + }, + { + name: 'Files', + value: ( + + ), + }, + ], + }, + ], + }, + }, + }, + { + id: 'charts', + type: 'charts', + renderContent: () => , + }, +]; diff --git a/tests/components/QueryExecutionPanel.test.tsx b/tests/components/QueryExecutionPanel.test.tsx index 4c7cdc7..3aeb4e6 100644 --- a/tests/components/QueryExecutionPanel.test.tsx +++ b/tests/components/QueryExecutionPanel.test.tsx @@ -1,8 +1,8 @@ // @vitest-environment jsdom import React, {act, useState} from 'react'; -import {createRoot, type Root} from 'react-dom/client'; +import {type Root, createRoot} from 'react-dom/client'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; -import {configure, ThemeProvider} from '@gravity-ui/uikit'; +import {ThemeProvider, configure} from '@gravity-ui/uikit'; import {QueryExecutionPanel} from '../../src/widgets/QueryExecutionPanel'; import type { QueryExecutionPanelProps, @@ -288,6 +288,36 @@ describe('QueryExecutionPanel', () => { click('Charts'); expect(panel().textContent).toContain('Count 1'); }); + it('preserves visited content through loading and retry, and pauses hidden content', () => { + const onRetry = vi.fn(); + render(); + act(() => panel().querySelector('button')!.click()); + render({loading: true, error: true, onRetry}); + expect(container.querySelector('[role="status"]')).not.toBeNull(); + expect(container.querySelector('[role="alert"]')).toBeNull(); + expect(panel()).toBeNull(); + expect(container.querySelector('[data-active="false"]')).not.toBeNull(); + expect(container.querySelector('[aria-busy="true"]')).not.toBeNull(); + render({error: true, onRetry}); + expect(container.querySelector('[role="alert"]')?.textContent).toContain('Error loading'); + act(() => container.querySelector('[role="alert"] button')!.click()); + expect(onRetry).toHaveBeenCalledOnce(); + render(); + expect(panel().textContent).toContain('Count 1'); + expect(selected()).toBe('a'); + expect(changes).not.toHaveBeenCalled(); + }); + it('does not mount unvisited content while loading', () => { + const renderContent = vi.fn(() => ); + const tabs: QueryExecutionTab[] = [ + {id: 'result', type: 'custom', title: 'Result', renderContent}, + ]; + render({tabs, loading: true}); + expect(renderContent).not.toHaveBeenCalled(); + render({tabs}); + expect(renderContent).toHaveBeenCalled(); + expect(panel().textContent).toContain('Count 0'); + }); it('renders metadata and delegates expand/close without remounting content', () => { const onClose = vi.fn(); const onExpandedChange = vi.fn(); diff --git a/tests/components/QueryProgress.test.tsx b/tests/components/QueryProgress.test.tsx index aea5653..41db649 100644 --- a/tests/components/QueryProgress.test.tsx +++ b/tests/components/QueryProgress.test.tsx @@ -12,6 +12,7 @@ vi.mock('@gravity-ui/uikit', () => { Flex: ({children}: any) =>
{children}
, Icon: () => null, Loader: () => null, + Label: ({children}: {children: React.ReactNode}) => {children}, SegmentedRadioGroup: Group, }; }); From 1750e365fd4a9d38668fd4347a4a80ce3ed5a9b0 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Mon, 28 Sep 2026 17:39:27 +0300 Subject: [PATCH 2/4] fix: collapse query execution panel to header --- src/types/queryExecutionPanel.ts | 5 + .../QueryExecutionPanel.scss | 5 + .../QueryExecutionPanel.tsx | 92 +++++++++++++------ src/widgets/QueryExecutionPanel/README.md | 19 +++- src/widgets/QueryExecutionPanel/i18n/en.json | 5 +- src/widgets/QueryExecutionPanel/i18n/ru.json | 5 +- .../story/QueryExecutionPanel.stories.tsx | 25 +---- tests/components/QueryExecutionPanel.test.tsx | 59 +++++++++++- 8 files changed, 159 insertions(+), 56 deletions(-) diff --git a/src/types/queryExecutionPanel.ts b/src/types/queryExecutionPanel.ts index d1486f5..b9711d8 100644 --- a/src/types/queryExecutionPanel.ts +++ b/src/types/queryExecutionPanel.ts @@ -53,6 +53,11 @@ export type QueryExecutionPanelProps< execution?: {startedAt?: ReactNode; author?: ReactNode}; expanded?: boolean; onExpandedChange?: (expanded: boolean) => void; + /** Collapse to the header without unmounting visited tabs. */ + collapsed?: boolean; + defaultCollapsed?: boolean; + onCollapsedChange?: (collapsed: boolean) => void; + /** @deprecated Use onCollapsedChange. Called when collapsing; do not unmount the panel. */ onClose?: () => void; /** Hide the content while the panel is loading, preserving visited tabs. */ loading?: boolean; diff --git a/src/widgets/QueryExecutionPanel/QueryExecutionPanel.scss b/src/widgets/QueryExecutionPanel/QueryExecutionPanel.scss index 733978d..f98abde 100644 --- a/src/widgets/QueryExecutionPanel/QueryExecutionPanel.scss +++ b/src/widgets/QueryExecutionPanel/QueryExecutionPanel.scss @@ -4,6 +4,11 @@ height: 100%; background: var(--g-color-base-background); + &_collapsed { + height: auto; + flex: 0 0 auto; + } + &__header { flex-shrink: 0; padding-inline: var(--g-spacing-4) var(--g-spacing-2); diff --git a/src/widgets/QueryExecutionPanel/QueryExecutionPanel.tsx b/src/widgets/QueryExecutionPanel/QueryExecutionPanel.tsx index 2a2ffc8..5f14722 100644 --- a/src/widgets/QueryExecutionPanel/QueryExecutionPanel.tsx +++ b/src/widgets/QueryExecutionPanel/QueryExecutionPanel.tsx @@ -1,5 +1,5 @@ -import React, {useEffect, useMemo} from 'react'; -import {ChevronsCollapseUpRight, ChevronsExpandUpRight, Xmark} from '@gravity-ui/icons'; +import React, {useEffect, useId, useMemo, useState} from 'react'; +import {ChevronsCollapseUpRight, ChevronsExpandUpRight, ChevronsUp, Xmark} from '@gravity-ui/icons'; import {Button, Flex, Icon, Tab, TabList, TabPanel, TabProvider, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; import type {NavigationMetaItem} from '../../types/navigation'; @@ -13,6 +13,21 @@ import './QueryExecutionPanel.scss'; const block = cn('qp-query-execution-panel'); +function ExecutionMetadata({execution}: Pick) { + return ( + <> + {execution?.startedAt !== undefined && execution.startedAt !== null && ( + {execution.startedAt} + )} + {execution?.author !== undefined && execution.author !== null && ( + + {i18n('context_by')} {execution.author} + + )} + + ); +} + export function QueryExecutionPanel< TRow extends Record = Record, TMetaItem extends NavigationMetaItem = NavigationMetaItem, @@ -25,6 +40,9 @@ export function QueryExecutionPanel< execution, expanded = false, onExpandedChange, + collapsed: controlledCollapsed, + defaultCollapsed = false, + onCollapsedChange, onClose, loading = false, error = false, @@ -32,6 +50,19 @@ export function QueryExecutionPanel< emptyContent, className, }: QueryExecutionPanelProps) { + const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(defaultCollapsed); + const collapsed = controlledCollapsed ?? uncontrolledCollapsed; + const contentId = useId(); + + const changeCollapsed = (next: boolean) => { + if (next === collapsed) return; + if (controlledCollapsed === undefined) setUncontrolledCollapsed(next); + onCollapsedChange?.(next); + if (next) { + if (expanded) onExpandedChange?.(false); + onClose?.(); + } + }; const {tabs, invalidIds} = useMemo(() => { const seen = new Set(); const invalid: string[] = []; @@ -60,7 +91,13 @@ export function QueryExecutionPanel< onActiveTabChange, }); const expandLabel = i18n(expanded ? 'action_collapse' : 'action_expand'); + const collapseLabel = i18n(collapsed ? 'action_show-content' : 'action_hide-content'); const showState = loading || error; + const selectAndReveal = (id: string) => { + if (!tabs.some((tab) => tab.id === id && !tab.disabled)) return; + changeCollapsed(false); + select(id); + }; const getTitle = (tab: QueryExecutionTab) => { if (tab.type === 'info') return i18n(`title_${getMessagesSeverity(tab.props?.root)}`); @@ -69,8 +106,8 @@ export function QueryExecutionPanel< }; return ( - - + + {tabs.map((tab) => ( @@ -82,7 +119,7 @@ export function QueryExecutionPanel< if (event.key === 'Enter' || event.key === ' ') { // Avoid a second selection from the button's native click. event.preventDefault(); - select(tab.id); + selectAndReveal(tab.id); } }} > @@ -91,14 +128,7 @@ export function QueryExecutionPanel< ))} - {execution?.startedAt !== undefined && execution.startedAt !== null && ( - {execution.startedAt} - )} - {execution?.author !== undefined && execution.author !== null && ( - - {i18n('context_by')} {execution.author} - - )} + {onExpandedChange && ( )} - {onClose && ( - - )} + -
+