Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ npm install @gravity-ui/querieskit
Your project must also provide:

- `react` / `react-dom` (^18 or ^19)
- `@gravity-ui/uikit` (>=7)
- `@gravity-ui/uikit` (>=7.18.0)
- `@gravity-ui/icons` (>=2)

See `peerDependencies` in `package.json` for the exact ranges.
Expand Down Expand Up @@ -295,3 +295,9 @@ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
## License

MIT — see [LICENSE](LICENSE).

### Execution timeline

`QueryTimeline` provides a virtualized execution timeline with stages, filters and live
updates. It can also be passed to `QueryProgress` using `timelineProps`. See the
[QueryTimeline guide](src/modules/QueryTimeline/README.md) for the data contract and examples.
45 changes: 43 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,12 @@
"lint:all": "eslint \"./src/**/*.{js,jsx,ts,tsx}\"",
"storybook": "storybook dev -p 6006 --no-open",
"build-storybook": "storybook build",
"test": "npm run test:timezones && npm run test:components",
"test": "npm run test:timezones && npm run test:components && npm run test:timeline",
"test:timezones": "TZ=UTC vitest run tests/helpers/time.test.ts && TZ=Europe/Moscow vitest run tests/helpers/time.test.ts && TZ=Asia/Kolkata vitest run tests/helpers/time.test.ts && TZ=America/New_York vitest run tests/helpers/time.test.ts",
"test:components": "vitest run tests/components",
"prepublishOnly": "npm run build",
"test:tree-shaking": "npm run build && node tests/tree-shaking/check.mjs"
"test:tree-shaking": "npm run build && node tests/tree-shaking/check.mjs",
"test:timeline": "vitest run tests/helpers/queryTimeline.test.ts"
},
"repository": {
"type": "git",
Expand All @@ -114,7 +115,6 @@
"devDependencies": {
"@commitlint/config-conventional": "^20.5.3",
"@gravity-ui/app-builder": "^0.49.0",
"@gravity-ui/date-utils": "^2.7.1",
"@gravity-ui/eslint-config": "^4.3.2",
"@gravity-ui/icons": "^2.20.0",
"@gravity-ui/prettier-config": "^1.1.1",
Expand All @@ -126,6 +126,7 @@
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/parser": "^8.64.0",
"esbuild": "^0.28.1",
"eslint": "^9.39.5",
"eslint-import-resolver-typescript": "^4.4.5",
"jsdom": "^26.1.0",
Expand All @@ -137,25 +138,27 @@
"typescript": "^6.0.3",
"vite": "^8.1.5",
"vite-plugin-svgr": "^5.2.0",
"vitest": "^3.2.4",
"esbuild": "^0.28.1"
"vitest": "^3.2.4"
},
"peerDependencies": {
"@gravity-ui/icons": ">=2.0.0",
"@gravity-ui/uikit": ">=7.0.0",
"@gravity-ui/uikit": ">=7.18.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"dependencies": {
"@gravity-ui/charts": "^1.57.2",
"@gravity-ui/dashkit": "^10.4.0",
"@gravity-ui/date-components": "^3.4.2",
"@gravity-ui/date-utils": "^2.7.1",
"@gravity-ui/graph": "1.12.1",
"@gravity-ui/i18n": "^1.8.0",
"@gravity-ui/illustrations": "^2.1.1",
"@gravity-ui/react-data-table": "^2.2.1",
"@gravity-ui/timeline": "1.32.2",
"@gravity-ui/unipika": "^5.6.3",
"bem-cn-lite": "^4.1.0",
"lodash": "^4.18.1",
"monaco-editor": "^0.56.0"
},
"overrides": {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './types/errorTree';
export * from './types/queryStatistics';
export * from './types/queryResults';
export * from './types/queryGraph';
export * from './types/queryTimeline';
13 changes: 1 addition & 12 deletions src/modules/QueryProgress/QueryProgress.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,6 @@
}

&__view_hidden {
visibility: hidden;
position: absolute;
pointer-events: none;
}

&__timeline {
min-height: 360px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed var(--g-color-line-generic);
border-radius: var(--g-border-radius-l);
display: none;
}
}
16 changes: 14 additions & 2 deletions src/modules/QueryProgress/QueryProgress.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ import React from 'react';
import type {Meta, StoryObj} from '@storybook/react';

import {demoEdges, demoNodes} from '../../components/QueryGraph/QueryGraph.stories';
import {timelineItems, timelineStatuses} from '../QueryTimeline/QueryTimeline.stories';
import {QueryProgress} from './QueryProgress';

const meta: Meta<typeof QueryProgress> = {
title: 'Modules/QueryProgress',
component: QueryProgress,
tags: ['autodocs'],
args: {graphProps: {nodes: demoNodes, edges: demoEdges}},
args: {
graphProps: {nodes: demoNodes, edges: demoEdges},
timelineProps: {
items: timelineItems,
statuses: timelineStatuses,
now: Date.UTC(2026, 8, 25, 10, 1, 20),
},
},
decorators: [
(Story) => (
<div style={{height: 640}}>
Expand All @@ -22,4 +30,8 @@ export default meta;
type Story = StoryObj<typeof QueryProgress>;

export const Default: Story = {};
export const TimelinePlaceholder: Story = {args: {defaultView: 'timeline'}};
export const Timeline: Story = {args: {defaultView: 'timeline'}};

export const WithoutTimelineData: Story = {
args: {defaultView: 'timeline', timelineProps: undefined},
};
56 changes: 40 additions & 16 deletions src/modules/QueryProgress/QueryProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {Suspense, useCallback, useMemo, useState} from 'react';
import React, {Suspense, useCallback, useEffect, useMemo, useState} from 'react';
import {
Circle,
CircleCheck,
Expand All @@ -7,7 +7,7 @@ import {
CirclePlay,
CircleXmark,
} from '@gravity-ui/icons';
import {Flex, Icon, Loader, SegmentedRadioGroup, Text} from '@gravity-ui/uikit';
import {Flex, Icon, Loader, SegmentedRadioGroup} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';

import type {
Expand All @@ -20,10 +20,14 @@ import i18n from './i18n';
import './QueryProgress.scss';

const QueryGraph = React.lazy(() =>
import('../../components/QueryGraph/QueryGraph').then((module) => ({
import('../../components/QueryGraph').then((module) => ({
default: module.QueryGraph,
})),
);
const QueryTimeline = React.lazy(() =>
import('../QueryTimeline').then((module) => ({default: module.QueryTimeline})),
);
const EMPTY_TIMELINE_ITEMS: NonNullable<QueryProgressProps['timelineProps']>['items'] = [];
const block = cn('qp-query-progress');
const statuses: QueryGraphNodeStatus[] = [
'not-started',
Expand All @@ -44,6 +48,7 @@ const statusIcons = {

export function QueryProgress({
graphProps,
timelineProps,
view: controlledView,
defaultView = 'graph',
onViewChange,
Expand All @@ -52,6 +57,11 @@ export function QueryProgress({
const [uncontrolledView, setUncontrolledView] = useState(defaultView);
const view = controlledView ?? uncontrolledView;
const [hasOpenedGraph, setHasOpenedGraph] = useState(view === 'graph');
const [hasOpenedTimeline, setHasOpenedTimeline] = useState(view === 'timeline');
useEffect(() => {
if (view === 'graph') setHasOpenedGraph(true);
if (view === 'timeline') setHasOpenedTimeline(true);
}, [view]);
const counts = useMemo(() => {
const result = Object.fromEntries(statuses.map((status) => [status, 0])) as Record<
QueryGraphNodeStatus,
Expand Down Expand Up @@ -84,26 +94,40 @@ export function QueryProgress({
{i18n('tab_timeline')}
</SegmentedRadioGroup.Option>
</SegmentedRadioGroup>
<Flex gap={2} wrap className={block('statuses')}>
{statuses.map((status) => (
<span key={status} className={block('status', {status})}>
<Icon data={statusIcons[status]} size={14} />
{i18n(`status_${status}`)}: {counts[status]}
</span>
))}
</Flex>
{view === 'graph' && (
<Flex gap={2} wrap className={block('statuses')}>
{statuses.map((status) => (
<span key={status} className={block('status', {status})}>
<Icon data={statusIcons[status]} size={14} />
{i18n(`status_${status}`)}: {counts[status]}
</span>
))}
</Flex>
)}
</Flex>
<div className={block('content')}>
{hasOpenedGraph && (
<div className={block('view', {hidden: view !== 'graph'})}>
{(hasOpenedGraph || view === 'graph') && (
<div
className={block('view', {hidden: view !== 'graph'})}
hidden={view !== 'graph'}
>
<Suspense fallback={<Loader size="m" />}>
<QueryGraph {...graphProps} active={view === 'graph'} />
</Suspense>
</div>
)}
{view === 'timeline' && (
<div className={block('timeline')}>
<Text color="secondary">{i18n('context_timeline-unavailable')}</Text>
{(hasOpenedTimeline || view === 'timeline') && (
<div
className={block('view', {hidden: view !== 'timeline'})}
hidden={view !== 'timeline'}
>
<Suspense fallback={<Loader size="m" />}>
<QueryTimeline
items={EMPTY_TIMELINE_ITEMS}
{...timelineProps}
active={view === 'timeline'}
/>
</Suspense>
</div>
)}
</div>
Expand Down
1 change: 0 additions & 1 deletion src/modules/QueryProgress/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"tab_graph": "Graph",
"tab_timeline": "Timeline",
"context_timeline-unavailable": "Timeline is not available yet",
"status_not-started": "Not started",
"status_waiting": "Waiting",
"status_running": "Running",
Expand Down
1 change: 0 additions & 1 deletion src/modules/QueryProgress/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"tab_graph": "Граф",
"tab_timeline": "Таймлайн",
"context_timeline-unavailable": "Таймлайн пока недоступен",
"status_not-started": "Не запущено",
"status_waiting": "Ожидание",
"status_running": "Выполняется",
Expand Down
51 changes: 51 additions & 0 deletions src/modules/QueryTimeline/QueryTimeline.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.qp-query-timeline {
height: 100%;
min-height: 160px;
min-width: 0;

&__header {
display: grid;
grid-template-columns: var(--qp-timeline-list-width) minmax(0, 1fr);
flex-shrink: 0;
}
&__heading {
padding: var(--g-spacing-2);
align-self: center;
}
&__ruler {
min-width: 0;
overflow: hidden;
}
&__scroll {
flex: 1;
min-height: 0;
overflow: auto;
position: relative;
}
&__spacer {
position: relative;
min-width: calc(var(--qp-timeline-list-width) + 160px);
}
&__rows {
position: absolute;
top: 0;
bottom: 0;
width: var(--qp-timeline-list-width);
}
&__row {
position: absolute;
width: 100%;
}
&__canvas {
position: sticky;
top: 0;
margin-left: var(--qp-timeline-list-width);
border-left: 1px solid var(--g-color-line-generic);
box-sizing: border-box;
}
&__state {
flex: 1;
height: 100%;
min-height: 100px;
}
}
Loading
Loading