diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d82855..8fc2e8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,20 @@ jobs: - name: Build and check widget tree-shaking run: npm run test:tree-shaking + - name: Check published package contracts + run: npm run test:package-contract + + - name: Bundle isolated package consumers + run: npm run test:consumer + + - name: Upload consumer bundle statistics + if: always() + uses: actions/upload-artifact@v4 + with: + name: consumer-bundle-stats + path: build/consumer + if-no-files-found: ignore + - name: Check browser test types run: npx tsc --noEmit -p tests/browser/tsconfig.json diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 35b2b85..7285840 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,6 +1,10 @@ +/// + import type {Decorator, Preview} from '@storybook/react'; import {ThemeProvider, configure} from '@gravity-ui/uikit'; -import React from 'react'; +import React, {useEffect, useState} from 'react'; +import {settings} from '@gravity-ui/date-utils'; +import {setLang} from '../src/i18n'; import EditorWorker from 'monaco-editor/editor/editor.worker?worker'; import JsonWorker from 'monaco-editor/languages/features/json/json.worker?worker'; @@ -18,21 +22,51 @@ self.MonacoEnvironment = { configure({lang: 'en'}); -const withYtComponentsTheme: Decorator = (Story, context) => { - const globalsTheme = context.globals.theme; - const isDark = globalsTheme === 'dark'; - const uikitTheme = isDark ? 'dark' : 'light'; +function LocalePreview({lang, children}: {lang: 'en' | 'ru'; children: React.ReactNode}) { + const [readyLang, setReadyLang] = useState(); + useEffect(() => { + let active = true; + settings.loadLocale(lang).then(() => { + if (!active) return; + settings.setLocale(lang); + configure({lang}); + setLang(lang); + setReadyLang(lang); + }); + return () => { + active = false; + }; + }, [lang]); + return readyLang === lang ? {children} : null; +} +const withYtComponentsTheme: Decorator = (Story, context) => { + const theme = context.globals.theme === 'dark' ? 'dark' : 'light'; + const lang = context.globals.locale === 'ru' ? 'ru' : 'en'; return ( - - - + + + + + ); }; const preview: Preview = { decorators: [withYtComponentsTheme], globalTypes: { + locale: { + defaultValue: 'en', + toolbar: { + title: 'Language', + icon: 'globe', + items: [ + {value: 'en', title: 'English'}, + {value: 'ru', title: 'Русский'}, + ], + dynamicTitle: true, + }, + }, theme: { defaultValue: 'light', toolbar: { diff --git a/AGENTS.md b/AGENTS.md index 47bf230..bd54523 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,7 +173,7 @@ Every component/module/widget/page that needs styles owns an eponymous `.scss` f - External positioning relative to a parent or siblings belongs to the parent. When a parent needs to apply such styles directly to a child, the child accepts `className` and merges it with its root BEM class; the parent passes its own BEM element, e.g. `className={block('child')}`. - Prefer `Flex` from `@gravity-ui/uikit` for flexbox layouts when its props can express the required behavior. Use props such as `direction`, `gap`, and `alignItems` instead of duplicating them with `display: flex` in SCSS. - Prefer container layout such as `Flex` with `gap` when it expresses the relationship; do not add a child class solely to duplicate container spacing. Keep flex rules in SCSS only when the `Flex` API cannot express the required selectors or behavior. -- Do not copy the current legacy patterns where `ClustersList.scss` defines `ClusterRow` styles or `NavigationItemsList.scss` defines `NavigationItemRow` styles. These are existing violations, not examples to follow. +- `ClusterRow` and `NavigationItemRow` own and import their styles. Preserve this ownership when composing them in list modules. For the full ownership rules, examples, and review checklist, see [`plans/styles-rules.md`](plans/styles-rules.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6c5f7..3bc788a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ * **QueryEditor:** add query link sharing dialog ([47c454d](https://github.com/gravity-ui/querieskit/commit/47c454daca5d1cfb9fb2001371701240585a53b9)) * **QueryEditor:** add save menu and simplify stories ([3062817](https://github.com/gravity-ui/querieskit/commit/3062817b75c080987726e1dda20788017c02767c)) * **QueryEditor:** integrate attachments in shared side panel ([c9e7c62](https://github.com/gravity-ui/querieskit/commit/c9e7c6298bd773efd435bc31762945f7c79f7f51)) -* **QueryEditor:** integrate settings and YTsaurus engine preset ([9c5a040](https://github.com/gravity-ui/querieskit/commit/9c5a0409de1ad51766b50b8572f9e4b448455886)) +* **QueryEditor:** integrate settings and built-in engine preset ([9c5a040](https://github.com/gravity-ui/querieskit/commit/9c5a0409de1ad51766b50b8572f9e4b448455886)) * **QuerySettings:** add extensible query settings form ([5d7321d](https://github.com/gravity-ui/querieskit/commit/5d7321db8b34f54a37f1ac18d3a7287ed40aa791)) diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..2390b50 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,69 @@ +# Migration notes for the library audit fixes + +These changes include breaking changes to engine factory names, preset imports, engine language descriptors and internal type paths. Component callback signatures and unrelated package entrypoints remain available. No release has been published as part of this change. + +## Query editor engines + +Use `createBuiltinQueryEngines` and the `Builtin`-prefixed public types: `BuiltinQueryEngineId`, `BuiltinEngineSettings`, `BuiltinQueryClique`, `BuiltinDiscoveryGroup` and `BuiltinQueryEnginesOptions`. The former branded factory and types have been removed without aliases. Import the preset from `@gravity-ui/querieskit/widgets/QueryEditor/presets/builtin`; both former branded preset subpaths have been removed, and no `modules` preset alias is provided. Within the repository, public types now live in `src/types/builtinQueryEngines.ts`. + +`createBuiltinQueryEngines()` now returns the same Monaco language IDs that `createQueryEditorPreset()` supports: + +| Engine ID (unchanged) | Previous language | Current language | +| --------------------- | ----------------- | ---------------- | +| `yql` | `yql` | `yql` | +| `chyt` | `clickhouse` | `clickhouse` | +| `spyt` | `s-expression` | `spyt` | +| `ql` | `yql_ansi` | `ytql` | + +Update application checks and custom providers that depended on the old SPYT/QL descriptors. The standalone `s-expression` and `yql_ansi` language registrations are retained for applications explicitly using them. + +Pass both UI configurations to the editor to enable engine settings and language services: + +```tsx +import {createQueryEditorPreset} from '@gravity-ui/querieskit/helpers/queryEditorPreset'; +import {createBuiltinQueryEngines} from '@gravity-ui/querieskit/widgets/QueryEditor/presets/builtin'; + +const engines = createBuiltinQueryEngines({ + yql: {versions: ['stable']}, + chyt: {cliques: [{alias: 'analytics', state: 'active', health: 'good'}]}, + spyt: {discoveryGroups: [{path: '//sys/spark/clusters/demo'}]}, +}); +const preset = createQueryEditorPreset({listPathChildren, getTableSchema, onReferenceClick}); +// Pass engines and preset in QueryEditor props or QueryPage tab.editorProps. +``` + +Keep these objects stable until their data changes. Applications still own requests, saved settings, query execution and navigation. QueryPage does not install engine or language presets automatically. The descriptors define UI controls; completion data and navigation are supplied through application callbacks. Its FourEngines story demonstrates all four engines with local adapters and independent tab drafts. + +## Public types + +Public props and consumer models now live in `src/types`. Import them from the package root or the corresponding unit entrypoint instead of implementation files and internal `types.ts` files: + +```ts +import type { + AddChartButtonOption, + Attachment, + AttachItem, + DashboardProps, + DashboardRenderItem, + HistoryFilterProps, + NavigationDetailProps, + SimpleFormProps, +} from '@gravity-ui/querieskit'; +``` + +In particular, replace deep imports of `components/Dashboard/types`, `modules/ChartEditor/types`, attachment editor internals and SimpleForm field implementation types. Old internal paths are not a compatibility contract. Unrelated package subpath aliases are retained. + +Within this repository, import types directly from their files. Dashboard types are in `src/types/dashboard.ts`, chart editor types in `chartEditor.ts`, attachment data in `attachments.ts`, attachment editing contracts in `attachmentsEditor.ts`, and chart button types in `addChartButton.ts`. Other extracted unit contracts use the corresponding camel-case `*Props.ts` file. HistoryFilter and NavigationDetail use `historyFilter.ts` and `navigationDetail.ts`; navigation resolver contracts use `navigationDetailResolvers.ts`. + +History row/search row, group/header and status/duration contracts now also have descriptive exported names such as `HistoryRowProps` and `QueryDurationProps` instead of a local generic `Props` name. Use `import type` for these dependencies. + +## Corrected behavior + +- HistoryFilter edits a local draft. Initial precedence is field defaults, then `initialValues`, then `values`. Apply sends the complete draft, including untouched fields. Reset restores field defaults plus `initialValues` and calls `onReset`. A substantive external `values` change replaces the draft; an equivalent object does not. Closing the popup keeps edits. +- Dashboard now calls the existing focus/blur callbacks with item content and its current index. Removed items do not emit callbacks. +- NavigationDetail restores a visible enabled tab after its uncontrolled selection disappears; automatic recovery does not emit a user selection callback. With no enabled tabs it renders empty content. Controlled selection remains application-owned. +- Default chart labels are localized. Custom text/options and technical chart IDs are unchanged. Count labels use “Fields: N” / “Elements: N” and their Russian equivalents. + +## Storybook + +Use the global Language and Theme controls to check EN/RU and light/dark. Switching language synchronizes querieskit, UIKit and date-utils and remounts the story, resetting demo state. Fixture data is no longer exported as stories. QueryShare controls and Actions now reflect the interactive example. diff --git a/README.md b/README.md index 317d1ea..d4cc87e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # QueriesKit · [![npm package](https://img.shields.io/npm/v/@gravity-ui/querieskit?logo=npm)](https://www.npmjs.com/package/@gravity-ui/querieskit) [![CI](https://img.shields.io/github/actions/workflow/status/gravity-ui/querieskit/ci.yml?branch=main&label=CI&logo=github)](https://github.com/gravity-ui/querieskit/actions/workflows/ci.yml?query=branch:main) [![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685?logo=storybook)](https://preview.gravity-ui.com/querieskit/) -React component library for building **query pages** — history, tutorials, editors, and related UI. Part of the [Gravity UI](https://gravity-ui.com) design system. +React component library for displaying query data and building **query pages** — history, tutorials, editors, and related UI. Part of the [Gravity UI](https://gravity-ui.com) design system. + +Applications supply data through props and handle callbacks. QueriesKit manages presentation and local UI interactions; it does not fetch data or execute queries. Data loading, query execution, persistence and routing belong to the application. The library is under active development and its public API is not yet stable. API changes may be released without a major version bump. Check the changelog @@ -64,7 +66,16 @@ Use pages for a complete screen layout, widgets for independent feature blocks, ### Individual imports -Pages, widgets, and modules support both root and individual imports: +All public components, modules, widgets, and pages support both root and individual imports. +Prefer individual imports to keep the dependency graph and extracted CSS focused on the units you use: + +```tsx +import {QueryStatusIcon} from '@gravity-ui/querieskit/components/QueryStatusIcon'; +import {QueriesHistory} from '@gravity-ui/querieskit/modules/QueriesHistory'; +import {QueryEditor} from '@gravity-ui/querieskit/widgets/QueryEditor'; +``` + +Root imports remain supported: ```tsx import {SavedQueries} from '@gravity-ui/querieskit'; @@ -77,9 +88,8 @@ import {SavedQueries} from '@gravity-ui/querieskit/modules/SavedQueries'; import type {SavedQueriesProps} from '@gravity-ui/querieskit/modules/SavedQueries'; ``` -Individual entrypoints are available for `QueryPage` under `pages/`, `QueriesHistory`, `SavedQueries`, -`TutorialsHistory`, `QueriesNavigation` under `modules/`, and `QueriesSidebar`, -`QueryEditor`, `QueryExecutionPanel`, `DashboardCharts` under `widgets/`. +Every public unit re-exported by the four level barrels has an individual entrypoint +under its corresponding level (`components/`, `modules/`, `widgets/`, or `pages/`). They expose each unit's existing public exports, including its props and helpers. Shared data types remain available from the package root. @@ -88,9 +98,23 @@ Both forms support tree-shaking with an ESM-aware bundler; individual imports do not necessarily produce a smaller bundle. Existing root imports remain supported. Keep CSS processing enabled so that the selected widget's styles are included. Some bundlers, including esbuild, retain CSS from unused root re-exports even when -their JavaScript is removed. Individual widget imports avoid introducing those +their JavaScript is removed. Individual unit imports avoid introducing those unrelated styles. +For browser builds, preserve ESM imports through to the bundler. Requiring the +package root as CommonJS eagerly traverses its re-exports and can retain unrelated +widgets and dependencies. Individual CJS entrypoints reduce that scope, but the +published CJS build does not preserve the ESM code-splitting boundaries. + +Search previews in `QueriesHistory`, `SavedQueries`, and `TutorialsHistory` load Monaco +on demand. While the editor chunk loads, the row displays the query as plain text +without changing its height or link. Configure production code splitting to retain +this loading boundary; a single-file bundle still contains the editor. Chunk-loading +errors propagate to the application's error boundary. + +See [the next-major migration notes](docs/migrations/next-major.md) for the explicit +Monaco language registrations and how to connect additional languages. + ### QueryPage Composes a shared `QueriesSidebar` with `QueryTabs`. Each query tab owns an independent @@ -146,9 +170,10 @@ Extensions stories. Individual imports are available from `components/MonacoEdit import {QueryEditor} from '@gravity-ui/querieskit/widgets/QueryEditor'; ``` -The YTsaurus preset is available from `widgets/QueryEditor/presets/ytsaurus`. -The previous package subpaths `modules/QueryEditor` and -`modules/QueryEditor/presets/ytsaurus` remain supported as compatibility aliases. +Optional engine UI configurations are available from `widgets/QueryEditor/presets/builtin`. +They define language selection, labels and settings controls using application-supplied data. +The previous package subpath `modules/QueryEditor` remains a compatibility alias for the widget. +The built-in preset is available only under the widget subpath. ### QueryExecutionPanel diff --git a/docs/editor-extensions.md b/docs/editor-extensions.md index df242a3..19eec22 100644 --- a/docs/editor-extensions.md +++ b/docs/editor-extensions.md @@ -7,6 +7,10 @@ container. Existing use without these props keeps its existing behavior. ## Language preset and application data +QueriesKit calls the supplied completion and reference callbacks. The application +provides the data (from local state or its own loading logic) and implements navigation. +The library itself does not fetch catalog data or execute queries. + ```tsx import {QueryEditor} from '@gravity-ui/querieskit'; import {createQueryEditorPreset} from '@gravity-ui/querieskit/helpers/queryEditorPreset'; diff --git a/docs/migrations/next-major.md b/docs/migrations/next-major.md new file mode 100644 index 0000000..e88a86b --- /dev/null +++ b/docs/migrations/next-major.md @@ -0,0 +1,58 @@ +# Next major: explicit Monaco languages and isolated imports + +This change is intended for the next major release. Component props, root exports, +and existing individual entrypoint aliases remain available. Automatic registration +of additional Monaco languages changes and may require application changes. + +## Languages + +QueriesKit registers its existing YQL (`yql`), YTQL (`yql_ansi`), CHYT (`clickhouse`), +SPYT (`s-expression`), and JSON (`json`) languages. It no longer imports Monaco's +root entrypoint, which implicitly registered unrelated languages and language services. +The editor's existing commands and contributions remain registered. + +Applications using other languages must register them before rendering the editor. +For example, with Monaco 0.56: + +```tsx +import 'monaco-editor/languages/definitions/python/register'; +import {MonacoEditor} from '@gravity-ui/querieskit/components/MonacoEditor'; + +; +``` + +Use the registration entrypoint for the desired language in your installed Monaco +version. Language services may also require their feature registrations and workers. +Keep the application and QueriesKit on the same resolved Monaco version so the +registrations reach the editor instance. Avoid importing Monaco's root merely to +access `editor` or `languages`; use `monaco-editor/editor/editor.api` at runtime. +Type-only imports from `monaco-editor` remain safe. + +## Imports and loading + +Every public component, module, widget, and page now has an explicit individual +entrypoint with ESM/CJS and TypeScript declarations. Prefer these entrypoints over +`build/...` paths or the root when bundle isolation matters: + +```tsx +import {QueryStatusIcon} from '@gravity-ui/querieskit/components/QueryStatusIcon'; +import {QueriesHistory} from '@gravity-ui/querieskit/modules/QueriesHistory'; +import {QueryEditor} from '@gravity-ui/querieskit/widgets/QueryEditor'; +``` + +Root imports still support JavaScript tree-shaking, but esbuild may extract CSS +from unused root re-exports. This release does not promise CSS isolation for root +imports. CSS remains side-effectful and must be processed by the application. + +History search previews now load the editor asynchronously when rendered. They +show plain query text at the same height while loading. Production code splitting +must remain enabled to keep Monaco out of the initial history chunk. A rejected +chunk load reaches the application's error boundary. + +## Runtime dependencies and browsers + +The package explicitly declares the Babel runtime 7 and core-js versions needed +by its generated JavaScript. An application may use Babel runtime 8 independently; +it must not force QueriesKit's runtime dependency to version 8 through overrides. +Browser targets and automatic polyfills are unchanged. Reducing polyfills is deferred +until supported browsers are agreed with consuming applications. diff --git a/package-lock.json b/package-lock.json index 7d1b105..e3dd5b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.6.0", "license": "MIT", "dependencies": { + "@babel/runtime": "^7.29.7", "@gravity-ui/charts": "^1.57.2", "@gravity-ui/dashkit": "^10.4.0", "@gravity-ui/date-components": "^3.4.2", @@ -21,6 +22,7 @@ "@gravity-ui/unipika": "^5.6.3", "@gravity-ui/websql-autocomplete": "^13.16.0", "bem-cn-lite": "^4.1.0", + "core-js": "^3.44.0", "lodash": "^4.18.1", "monaco-editor": "^0.56.0" }, @@ -39,19 +41,24 @@ "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.64.0", "@typescript-eslint/parser": "^8.64.0", + "css-loader": "^7.1.4", "esbuild": "^0.28.1", "eslint": "^9.39.5", "eslint-import-resolver-typescript": "^4.4.5", "jsdom": "^26.1.0", + "mini-css-extract-plugin": "^2.10.2", "prettier": "^3.9.5", "react": "^19.2.7", "react-dom": "^19.2.7", + "sass": "^1.101.0", + "sass-loader": "^16.0.8", "storybook": "^10.5.0", "tsx": "^4.23.13", "typescript": "^6.0.3", "vite": "^8.1.5", "vite-plugin-svgr": "^5.2.0", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "webpack": "^5.108.4" }, "peerDependencies": { "@gravity-ui/icons": ">=2.0.0", @@ -2309,11 +2316,13 @@ } }, "node_modules/@babel/runtime": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.5.tgz", - "integrity": "sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", "license": "MIT", - "peer": true + "engines": { + "node": ">=6.9.0" + } }, "node_modules/@babel/template": { "version": "7.29.7", @@ -4442,16 +4451,6 @@ } } }, - "node_modules/@gravity-ui/app-builder/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@gravity-ui/app-builder/node_modules/@module-federation/dts-plugin": { "version": "0.22.1", "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.22.1.tgz", @@ -5253,15 +5252,6 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@hello-pangea/dnd/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -10973,16 +10963,6 @@ "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@testing-library/dom/node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -15506,7 +15486,6 @@ "version": "3.44.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "funding": { @@ -16782,15 +16761,6 @@ "csstype": "^3.0.2" } }, - "node_modules/dom-helpers/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -25414,15 +25384,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/rc-slider/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/rc-util": { "version": "5.44.4", "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", @@ -25437,15 +25398,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/rc-util/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -25663,15 +25615,6 @@ "react-dom": ">=16.6.0" } }, - "node_modules/react-transition-group/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/react-virtualized-auto-sizer": { "version": "1.0.26", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", @@ -25699,15 +25642,6 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-window/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", diff --git a/package.json b/package.json index 1d7ec2b..d31d141 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,6 @@ "default": "./build/cjs/modules/QueryShare/index.js" } }, - "./modules/QueryEditor/presets/ytsaurus": { - "import": { - "types": "./build/esm/widgets/QueryEditor/presets/ytsaurus/index.d.ts", - "default": "./build/esm/widgets/QueryEditor/presets/ytsaurus/index.js" - }, - "require": { - "types": "./build/cjs/widgets/QueryEditor/presets/ytsaurus/index.d.ts", - "default": "./build/cjs/widgets/QueryEditor/presets/ytsaurus/index.js" - } - }, "./modules/QuerySettings": { "import": { "types": "./build/esm/modules/QuerySettings/index.d.ts", @@ -236,14 +226,14 @@ "default": "./build/cjs/widgets/QueryEditor/index.js" } }, - "./widgets/QueryEditor/presets/ytsaurus": { + "./widgets/QueryEditor/presets/builtin": { "import": { - "types": "./build/esm/widgets/QueryEditor/presets/ytsaurus/index.d.ts", - "default": "./build/esm/widgets/QueryEditor/presets/ytsaurus/index.js" + "types": "./build/esm/widgets/QueryEditor/presets/builtin/index.d.ts", + "default": "./build/esm/widgets/QueryEditor/presets/builtin/index.js" }, "require": { - "types": "./build/cjs/widgets/QueryEditor/presets/ytsaurus/index.d.ts", - "default": "./build/cjs/widgets/QueryEditor/presets/ytsaurus/index.js" + "types": "./build/cjs/widgets/QueryEditor/presets/builtin/index.d.ts", + "default": "./build/cjs/widgets/QueryEditor/presets/builtin/index.js" } }, "./pages/QueryPage": { @@ -275,10 +265,601 @@ "types": "./build/cjs/modules/QueryComparison/index.d.ts", "default": "./build/cjs/modules/QueryComparison/index.js" } + }, + "./components/DataTable": { + "import": { + "types": "./build/esm/components/DataTable/index.d.ts", + "default": "./build/esm/components/DataTable/index.js" + }, + "require": { + "types": "./build/cjs/components/DataTable/index.d.ts", + "default": "./build/cjs/components/DataTable/index.js" + } + }, + "./components/QueryResultsTable": { + "import": { + "types": "./build/esm/components/QueryResultsTable/index.d.ts", + "default": "./build/esm/components/QueryResultsTable/index.js" + }, + "require": { + "types": "./build/cjs/components/QueryResultsTable/index.d.ts", + "default": "./build/cjs/components/QueryResultsTable/index.js" + } + }, + "./components/QueryStatusIcon": { + "import": { + "types": "./build/esm/components/QueryStatusIcon/index.d.ts", + "default": "./build/esm/components/QueryStatusIcon/index.js" + }, + "require": { + "types": "./build/cjs/components/QueryStatusIcon/index.d.ts", + "default": "./build/cjs/components/QueryStatusIcon/index.js" + } + }, + "./components/QueryDuration": { + "import": { + "types": "./build/esm/components/QueryDuration/index.d.ts", + "default": "./build/esm/components/QueryDuration/index.js" + }, + "require": { + "types": "./build/cjs/components/QueryDuration/index.d.ts", + "default": "./build/cjs/components/QueryDuration/index.js" + } + }, + "./components/LazyList": { + "import": { + "types": "./build/esm/components/LazyList/index.d.ts", + "default": "./build/esm/components/LazyList/index.js" + }, + "require": { + "types": "./build/cjs/components/LazyList/index.d.ts", + "default": "./build/cjs/components/LazyList/index.js" + } + }, + "./components/ListSpinner": { + "import": { + "types": "./build/esm/components/ListSpinner/index.d.ts", + "default": "./build/esm/components/ListSpinner/index.js" + }, + "require": { + "types": "./build/cjs/components/ListSpinner/index.d.ts", + "default": "./build/cjs/components/ListSpinner/index.js" + } + }, + "./components/Breadcrumbs": { + "import": { + "types": "./build/esm/components/Breadcrumbs/index.d.ts", + "default": "./build/esm/components/Breadcrumbs/index.js" + }, + "require": { + "types": "./build/cjs/components/Breadcrumbs/index.d.ts", + "default": "./build/cjs/components/Breadcrumbs/index.js" + } + }, + "./components/PathEditor": { + "import": { + "types": "./build/esm/components/PathEditor/index.d.ts", + "default": "./build/esm/components/PathEditor/index.js" + }, + "require": { + "types": "./build/cjs/components/PathEditor/index.d.ts", + "default": "./build/cjs/components/PathEditor/index.js" + } + }, + "./components/HistoryFilter": { + "import": { + "types": "./build/esm/components/HistoryFilter/index.d.ts", + "default": "./build/esm/components/HistoryFilter/index.js" + }, + "require": { + "types": "./build/cjs/components/HistoryFilter/index.d.ts", + "default": "./build/cjs/components/HistoryFilter/index.js" + } + }, + "./components/HistoryPrivateIcon": { + "import": { + "types": "./build/esm/components/HistoryPrivateIcon/index.d.ts", + "default": "./build/esm/components/HistoryPrivateIcon/index.js" + }, + "require": { + "types": "./build/cjs/components/HistoryPrivateIcon/index.d.ts", + "default": "./build/cjs/components/HistoryPrivateIcon/index.js" + } + }, + "./components/FieldsSelector": { + "import": { + "types": "./build/esm/components/FieldsSelector/index.d.ts", + "default": "./build/esm/components/FieldsSelector/index.js" + }, + "require": { + "types": "./build/cjs/components/FieldsSelector/index.d.ts", + "default": "./build/cjs/components/FieldsSelector/index.js" + } + }, + "./components/EditableRowTitle": { + "import": { + "types": "./build/esm/components/EditableRowTitle/index.d.ts", + "default": "./build/esm/components/EditableRowTitle/index.js" + }, + "require": { + "types": "./build/cjs/components/EditableRowTitle/index.d.ts", + "default": "./build/cjs/components/EditableRowTitle/index.js" + } + }, + "./components/RowActionsMenu": { + "import": { + "types": "./build/esm/components/RowActionsMenu/index.d.ts", + "default": "./build/esm/components/RowActionsMenu/index.js" + }, + "require": { + "types": "./build/cjs/components/RowActionsMenu/index.d.ts", + "default": "./build/cjs/components/RowActionsMenu/index.js" + } + }, + "./components/FieldsSearchToolbar": { + "import": { + "types": "./build/esm/components/FieldsSearchToolbar/index.d.ts", + "default": "./build/esm/components/FieldsSearchToolbar/index.js" + }, + "require": { + "types": "./build/cjs/components/FieldsSearchToolbar/index.d.ts", + "default": "./build/cjs/components/FieldsSearchToolbar/index.js" + } + }, + "./components/HistoryGroupHeader": { + "import": { + "types": "./build/esm/components/HistoryGroupHeader/index.d.ts", + "default": "./build/esm/components/HistoryGroupHeader/index.js" + }, + "require": { + "types": "./build/cjs/components/HistoryGroupHeader/index.d.ts", + "default": "./build/cjs/components/HistoryGroupHeader/index.js" + } + }, + "./components/EmptyContent": { + "import": { + "types": "./build/esm/components/EmptyContent/index.d.ts", + "default": "./build/esm/components/EmptyContent/index.js" + }, + "require": { + "types": "./build/cjs/components/EmptyContent/index.d.ts", + "default": "./build/cjs/components/EmptyContent/index.js" + } + }, + "./components/ClusterRow": { + "import": { + "types": "./build/esm/components/ClusterRow/index.d.ts", + "default": "./build/esm/components/ClusterRow/index.js" + }, + "require": { + "types": "./build/cjs/components/ClusterRow/index.d.ts", + "default": "./build/cjs/components/ClusterRow/index.js" + } + }, + "./components/NavigationItemRow": { + "import": { + "types": "./build/esm/components/NavigationItemRow/index.d.ts", + "default": "./build/esm/components/NavigationItemRow/index.js" + }, + "require": { + "types": "./build/cjs/components/NavigationItemRow/index.d.ts", + "default": "./build/cjs/components/NavigationItemRow/index.js" + } + }, + "./components/NavigationActionButtons": { + "import": { + "types": "./build/esm/components/NavigationActionButtons/index.d.ts", + "default": "./build/esm/components/NavigationActionButtons/index.js" + }, + "require": { + "types": "./build/cjs/components/NavigationActionButtons/index.d.ts", + "default": "./build/cjs/components/NavigationActionButtons/index.js" + } + }, + "./components/SkeletonRows": { + "import": { + "types": "./build/esm/components/SkeletonRows/index.d.ts", + "default": "./build/esm/components/SkeletonRows/index.js" + }, + "require": { + "types": "./build/cjs/components/SkeletonRows/index.d.ts", + "default": "./build/cjs/components/SkeletonRows/index.js" + } + }, + "./components/RowLink": { + "import": { + "types": "./build/esm/components/RowLink/index.d.ts", + "default": "./build/esm/components/RowLink/index.js" + }, + "require": { + "types": "./build/cjs/components/RowLink/index.d.ts", + "default": "./build/cjs/components/RowLink/index.js" + } + }, + "./components/SearchRowLayout": { + "import": { + "types": "./build/esm/components/SearchRowLayout/index.d.ts", + "default": "./build/esm/components/SearchRowLayout/index.js" + }, + "require": { + "types": "./build/cjs/components/SearchRowLayout/index.d.ts", + "default": "./build/cjs/components/SearchRowLayout/index.js" + } + }, + "./components/SearchWithButtons": { + "import": { + "types": "./build/esm/components/SearchWithButtons/index.d.ts", + "default": "./build/esm/components/SearchWithButtons/index.js" + }, + "require": { + "types": "./build/cjs/components/SearchWithButtons/index.d.ts", + "default": "./build/cjs/components/SearchWithButtons/index.js" + } + }, + "./components/AddChartButton": { + "import": { + "types": "./build/esm/components/AddChartButton/index.d.ts", + "default": "./build/esm/components/AddChartButton/index.js" + }, + "require": { + "types": "./build/cjs/components/AddChartButton/index.d.ts", + "default": "./build/cjs/components/AddChartButton/index.js" + } + }, + "./components/ChartEditorForm": { + "import": { + "types": "./build/esm/components/ChartEditorForm/index.d.ts", + "default": "./build/esm/components/ChartEditorForm/index.js" + }, + "require": { + "types": "./build/cjs/components/ChartEditorForm/index.d.ts", + "default": "./build/cjs/components/ChartEditorForm/index.js" + } + }, + "./components/FormField": { + "import": { + "types": "./build/esm/components/FormField/index.d.ts", + "default": "./build/esm/components/FormField/index.js" + }, + "require": { + "types": "./build/cjs/components/FormField/index.d.ts", + "default": "./build/cjs/components/FormField/index.js" + } + }, + "./components/ChartFieldsForm": { + "import": { + "types": "./build/esm/components/ChartFieldsForm/index.d.ts", + "default": "./build/esm/components/ChartFieldsForm/index.js" + }, + "require": { + "types": "./build/cjs/components/ChartFieldsForm/index.d.ts", + "default": "./build/cjs/components/ChartFieldsForm/index.js" + } + }, + "./components/ChartConfigFields": { + "import": { + "types": "./build/esm/components/ChartConfigFields/index.d.ts", + "default": "./build/esm/components/ChartConfigFields/index.js" + }, + "require": { + "types": "./build/cjs/components/ChartConfigFields/index.d.ts", + "default": "./build/cjs/components/ChartConfigFields/index.js" + } + }, + "./components/Dashboard": { + "import": { + "types": "./build/esm/components/Dashboard/index.d.ts", + "default": "./build/esm/components/Dashboard/index.js" + }, + "require": { + "types": "./build/cjs/components/Dashboard/index.d.ts", + "default": "./build/cjs/components/Dashboard/index.js" + } + }, + "./components/Chart": { + "import": { + "types": "./build/esm/components/Chart/index.d.ts", + "default": "./build/esm/components/Chart/index.js" + }, + "require": { + "types": "./build/cjs/components/Chart/index.d.ts", + "default": "./build/cjs/components/Chart/index.js" + } + }, + "./components/ErrorTree": { + "import": { + "types": "./build/esm/components/ErrorTree/index.d.ts", + "default": "./build/esm/components/ErrorTree/index.js" + }, + "require": { + "types": "./build/cjs/components/ErrorTree/index.d.ts", + "default": "./build/cjs/components/ErrorTree/index.js" + } + }, + "./components/AttachmentList": { + "import": { + "types": "./build/esm/components/AttachmentList/index.d.ts", + "default": "./build/esm/components/AttachmentList/index.js" + }, + "require": { + "types": "./build/cjs/components/AttachmentList/index.d.ts", + "default": "./build/cjs/components/AttachmentList/index.js" + } + }, + "./components/AttachmentListPlaceholder": { + "import": { + "types": "./build/esm/components/AttachmentListPlaceholder/index.d.ts", + "default": "./build/esm/components/AttachmentListPlaceholder/index.js" + }, + "require": { + "types": "./build/cjs/components/AttachmentListPlaceholder/index.d.ts", + "default": "./build/cjs/components/AttachmentListPlaceholder/index.js" + } + }, + "./components/QueryGraph": { + "import": { + "types": "./build/esm/components/QueryGraph/index.d.ts", + "default": "./build/esm/components/QueryGraph/index.js" + }, + "require": { + "types": "./build/cjs/components/QueryGraph/index.d.ts", + "default": "./build/cjs/components/QueryGraph/index.js" + } + }, + "./modules/HistoryHeader": { + "import": { + "types": "./build/esm/modules/HistoryHeader/index.d.ts", + "default": "./build/esm/modules/HistoryHeader/index.js" + }, + "require": { + "types": "./build/cjs/modules/HistoryHeader/index.d.ts", + "default": "./build/cjs/modules/HistoryHeader/index.js" + } + }, + "./modules/HistoryLayout": { + "import": { + "types": "./build/esm/modules/HistoryLayout/index.d.ts", + "default": "./build/esm/modules/HistoryLayout/index.js" + }, + "require": { + "types": "./build/cjs/modules/HistoryLayout/index.d.ts", + "default": "./build/cjs/modules/HistoryLayout/index.js" + } + }, + "./modules/HistoryComparisonActions": { + "import": { + "types": "./build/esm/modules/HistoryComparisonActions/index.d.ts", + "default": "./build/esm/modules/HistoryComparisonActions/index.js" + }, + "require": { + "types": "./build/cjs/modules/HistoryComparisonActions/index.d.ts", + "default": "./build/cjs/modules/HistoryComparisonActions/index.js" + } + }, + "./modules/NavigationHeader": { + "import": { + "types": "./build/esm/modules/NavigationHeader/index.d.ts", + "default": "./build/esm/modules/NavigationHeader/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationHeader/index.d.ts", + "default": "./build/cjs/modules/NavigationHeader/index.js" + } + }, + "./modules/RowsList": { + "import": { + "types": "./build/esm/modules/RowsList/index.d.ts", + "default": "./build/esm/modules/RowsList/index.js" + }, + "require": { + "types": "./build/cjs/modules/RowsList/index.d.ts", + "default": "./build/cjs/modules/RowsList/index.js" + } + }, + "./modules/QueriesList": { + "import": { + "types": "./build/esm/modules/QueriesList/index.d.ts", + "default": "./build/esm/modules/QueriesList/index.js" + }, + "require": { + "types": "./build/cjs/modules/QueriesList/index.d.ts", + "default": "./build/cjs/modules/QueriesList/index.js" + } + }, + "./modules/HistoryRow": { + "import": { + "types": "./build/esm/modules/HistoryRow/index.d.ts", + "default": "./build/esm/modules/HistoryRow/index.js" + }, + "require": { + "types": "./build/cjs/modules/HistoryRow/index.d.ts", + "default": "./build/cjs/modules/HistoryRow/index.js" + } + }, + "./modules/HistorySearchRow": { + "import": { + "types": "./build/esm/modules/HistorySearchRow/index.d.ts", + "default": "./build/esm/modules/HistorySearchRow/index.js" + }, + "require": { + "types": "./build/cjs/modules/HistorySearchRow/index.d.ts", + "default": "./build/cjs/modules/HistorySearchRow/index.js" + } + }, + "./modules/SavedQueryRow": { + "import": { + "types": "./build/esm/modules/SavedQueryRow/index.d.ts", + "default": "./build/esm/modules/SavedQueryRow/index.js" + }, + "require": { + "types": "./build/cjs/modules/SavedQueryRow/index.d.ts", + "default": "./build/cjs/modules/SavedQueryRow/index.js" + } + }, + "./modules/SavedQuerySearchRow": { + "import": { + "types": "./build/esm/modules/SavedQuerySearchRow/index.d.ts", + "default": "./build/esm/modules/SavedQuerySearchRow/index.js" + }, + "require": { + "types": "./build/cjs/modules/SavedQuerySearchRow/index.d.ts", + "default": "./build/cjs/modules/SavedQuerySearchRow/index.js" + } + }, + "./modules/ChartEditor": { + "import": { + "types": "./build/esm/modules/ChartEditor/index.d.ts", + "default": "./build/esm/modules/ChartEditor/index.js" + }, + "require": { + "types": "./build/cjs/modules/ChartEditor/index.d.ts", + "default": "./build/cjs/modules/ChartEditor/index.js" + } + }, + "./modules/Attachments": { + "import": { + "types": "./build/esm/modules/Attachments/index.d.ts", + "default": "./build/esm/modules/Attachments/index.js" + }, + "require": { + "types": "./build/cjs/modules/Attachments/index.d.ts", + "default": "./build/cjs/modules/Attachments/index.js" + } + }, + "./modules/TutorialRow": { + "import": { + "types": "./build/esm/modules/TutorialRow/index.d.ts", + "default": "./build/esm/modules/TutorialRow/index.js" + }, + "require": { + "types": "./build/cjs/modules/TutorialRow/index.d.ts", + "default": "./build/cjs/modules/TutorialRow/index.js" + } + }, + "./modules/TutorialSearchRow": { + "import": { + "types": "./build/esm/modules/TutorialSearchRow/index.d.ts", + "default": "./build/esm/modules/TutorialSearchRow/index.js" + }, + "require": { + "types": "./build/cjs/modules/TutorialSearchRow/index.d.ts", + "default": "./build/cjs/modules/TutorialSearchRow/index.js" + } + }, + "./modules/ClustersList": { + "import": { + "types": "./build/esm/modules/ClustersList/index.d.ts", + "default": "./build/esm/modules/ClustersList/index.js" + }, + "require": { + "types": "./build/cjs/modules/ClustersList/index.d.ts", + "default": "./build/cjs/modules/ClustersList/index.js" + } + }, + "./modules/NavigationItemsList": { + "import": { + "types": "./build/esm/modules/NavigationItemsList/index.d.ts", + "default": "./build/esm/modules/NavigationItemsList/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationItemsList/index.d.ts", + "default": "./build/cjs/modules/NavigationItemsList/index.js" + } + }, + "./modules/NavigationDetail": { + "import": { + "types": "./build/esm/modules/NavigationDetail/index.d.ts", + "default": "./build/esm/modules/NavigationDetail/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationDetail/index.d.ts", + "default": "./build/cjs/modules/NavigationDetail/index.js" + } + }, + "./modules/NavigationSchema": { + "import": { + "types": "./build/esm/modules/NavigationSchema/index.d.ts", + "default": "./build/esm/modules/NavigationSchema/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationSchema/index.d.ts", + "default": "./build/cjs/modules/NavigationSchema/index.js" + } + }, + "./modules/QueryStatistics": { + "import": { + "types": "./build/esm/modules/QueryStatistics/index.d.ts", + "default": "./build/esm/modules/QueryStatistics/index.js" + }, + "require": { + "types": "./build/cjs/modules/QueryStatistics/index.d.ts", + "default": "./build/cjs/modules/QueryStatistics/index.js" + } + }, + "./modules/QueryProgress": { + "import": { + "types": "./build/esm/modules/QueryProgress/index.d.ts", + "default": "./build/esm/modules/QueryProgress/index.js" + }, + "require": { + "types": "./build/cjs/modules/QueryProgress/index.d.ts", + "default": "./build/cjs/modules/QueryProgress/index.js" + } + }, + "./modules/NavigationPreview": { + "import": { + "types": "./build/esm/modules/NavigationPreview/index.d.ts", + "default": "./build/esm/modules/NavigationPreview/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationPreview/index.d.ts", + "default": "./build/cjs/modules/NavigationPreview/index.js" + } + }, + "./modules/NavigationMeta": { + "import": { + "types": "./build/esm/modules/NavigationMeta/index.d.ts", + "default": "./build/esm/modules/NavigationMeta/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationMeta/index.d.ts", + "default": "./build/cjs/modules/NavigationMeta/index.js" + } + }, + "./modules/NavigationView": { + "import": { + "types": "./build/esm/modules/NavigationView/index.d.ts", + "default": "./build/esm/modules/NavigationView/index.js" + }, + "require": { + "types": "./build/cjs/modules/NavigationView/index.d.ts", + "default": "./build/cjs/modules/NavigationView/index.js" + } + }, + "./modules/QueryTimeline": { + "import": { + "types": "./build/esm/modules/QueryTimeline/index.d.ts", + "default": "./build/esm/modules/QueryTimeline/index.js" + }, + "require": { + "types": "./build/cjs/modules/QueryTimeline/index.d.ts", + "default": "./build/cjs/modules/QueryTimeline/index.js" + } + }, + "./modules/ChartFieldsEditor": { + "import": { + "types": "./build/esm/modules/ChartFieldsEditor/index.d.ts", + "default": "./build/esm/modules/ChartFieldsEditor/index.js" + }, + "require": { + "types": "./build/cjs/modules/ChartFieldsEditor/index.d.ts", + "default": "./build/cjs/modules/ChartFieldsEditor/index.js" + } } }, "files": [ - "build" + "build/esm", + "build/cjs" ], "scripts": { "build": "app-builder build", @@ -287,7 +868,7 @@ "storybook": "storybook dev -p 6006 --no-open", "build-storybook": "storybook build", "test": "npm run test:timezones && npm run test:components && npm run test:timeline && npm run test:charts && npm run test:editor-helpers && npm run test:parameters && npm run test:types", - "test:charts": "vitest run tests/helpers/chartFieldsForm.test.ts tests/helpers/chartBindingsExamples.test.ts", + "test:charts": "vitest run tests/helpers/chartFieldsForm.test.ts tests/helpers/chartBindingsExamples.test.ts tests/helpers/chartLabels.test.ts", "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", @@ -297,7 +878,9 @@ "test:editors:browser": "playwright test", "test:parameters": "vitest run tests/helpers/queryParameters.test.ts tests/helpers/parameterDate.test.ts", "test:types": "tsc -p tests/types", - "test:page:browser": "playwright test tests/browser/queryPage.spec.ts" + "test:page:browser": "playwright test tests/browser/queryPage.spec.ts", + "test:package-contract": "node tests/tree-shaking/check-published.mjs", + "test:consumer": "node tests/tree-shaking/consumer.mjs" }, "repository": { "type": "git", @@ -323,19 +906,24 @@ "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.64.0", "@typescript-eslint/parser": "^8.64.0", + "css-loader": "^7.1.4", "esbuild": "^0.28.1", "eslint": "^9.39.5", "eslint-import-resolver-typescript": "^4.4.5", "jsdom": "^26.1.0", + "mini-css-extract-plugin": "^2.10.2", "prettier": "^3.9.5", "react": "^19.2.7", "react-dom": "^19.2.7", + "sass": "^1.101.0", + "sass-loader": "^16.0.8", "storybook": "^10.5.0", "tsx": "^4.23.13", "typescript": "^6.0.3", "vite": "^8.1.5", "vite-plugin-svgr": "^5.2.0", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "webpack": "^5.108.4" }, "peerDependencies": { "@gravity-ui/icons": ">=2.0.0", @@ -344,6 +932,7 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "dependencies": { + "@babel/runtime": "^7.29.7", "@gravity-ui/charts": "^1.57.2", "@gravity-ui/dashkit": "^10.4.0", "@gravity-ui/date-components": "^3.4.2", @@ -356,6 +945,7 @@ "@gravity-ui/unipika": "^5.6.3", "@gravity-ui/websql-autocomplete": "^13.16.0", "bem-cn-lite": "^4.1.0", + "core-js": "^3.44.0", "lodash": "^4.18.1", "monaco-editor": "^0.56.0" }, @@ -374,6 +964,8 @@ "./build/*/components/MonacoEditor/monaco-yql-languages/yql/yql.contribution.js", "./build/*/components/MonacoEditor/monaco-yql-languages/yql_ansi/yql_ansi.contribution.js", "./build/*/components/MonacoEditor/monaco-yql-languages/s-expressions/s-expressions.contribution.js", - "./build/*/components/MonacoEditor/monaco-yql-languages/themes/themes.contribution.js" + "./build/*/components/MonacoEditor/monaco-yql-languages/themes/themes.contribution.js", + "./src/components/MonacoEditor/initialize.ts", + "./build/*/components/MonacoEditor/initialize.js" ] } diff --git a/plans/styles-rules.md b/plans/styles-rules.md index 82d31b6..1e117f6 100644 --- a/plans/styles-rules.md +++ b/plans/styles-rules.md @@ -22,12 +22,10 @@ import './Child.scss'; const block = cn('qp-child'); export type ChildProps = { - className?: string; + className?: string; }; -export const Child = ({className}: ChildProps) => ( -
...
-); +export const Child = ({className}: ChildProps) =>
...
; ``` ```scss @@ -47,10 +45,10 @@ export const Child = ({className}: ChildProps) => ( import {Flex} from '@gravity-ui/uikit'; export const Parent = () => ( - - - - + + + + ); ``` @@ -118,12 +116,12 @@ export const Parent = () => ; Такой подход связывает внешний вид ребёнка с конкретным родителем: при самостоятельном использовании или переносе ребёнка его стили не загрузятся. Родительский SCSS должен описывать BEM-блок родителя и его элементы, но не BEM-блоки самостоятельных дочерних компонентов. -В текущем коде есть исторические нарушения этого правила: +Ранее стили `ClusterRow` и `NavigationItemRow` загружались через родительские списки. Теперь каждый компонент владеет своим SCSS-файлом и импортирует его самостоятельно: -- [`ClustersList.scss`](../src/modules/ClustersList/ClustersList.scss) объявляет блок `.qp-cluster-row`, принадлежащий [`ClusterRow`](../src/components/ClusterRow/ClusterRow.tsx). -- [`NavigationItemsList.scss`](../src/modules/NavigationItemsList/NavigationItemsList.scss) объявляет блок `.qp-navigation-item-row`, принадлежащий [`NavigationItemRow`](../src/components/NavigationItemRow/NavigationItemRow.tsx). +- [`ClusterRow.scss`](../src/components/ClusterRow/ClusterRow.scss) импортируется из [`ClusterRow.tsx`](../src/components/ClusterRow/ClusterRow.tsx). +- [`NavigationItemRow.scss`](../src/components/NavigationItemRow/NavigationItemRow.scss) импортируется из [`NavigationItemRow.tsx`](../src/components/NavigationItemRow/NavigationItemRow.tsx). -Эти места зафиксированы как анти-примеры и не должны использоваться как образец для нового кода. Их исправление выполняется отдельно. +Сохраняйте это разделение при изменении списков: родитель отвечает за внешнюю компоновку, ребёнок — за собственное оформление. ## Чек-лист diff --git a/src/components/AddChartButton/AddChartButton.tsx b/src/components/AddChartButton/AddChartButton.tsx index 2cee68d..af6608c 100644 --- a/src/components/AddChartButton/AddChartButton.tsx +++ b/src/components/AddChartButton/AddChartButton.tsx @@ -1,4 +1,3 @@ -import React, {type ReactNode, useMemo, useState} from 'react'; import { ChartAreaStacked, ChartBar, @@ -9,66 +8,82 @@ import { Plus, SquareBars, } from '@gravity-ui/icons'; -import { - Button, - type ButtonButtonProps, - DropdownMenu, - type DropdownMenuProps, - Icon, - type IconData, -} from '@gravity-ui/uikit'; +import {Button, DropdownMenu, Icon} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {useState} from 'react'; +import type { + AddChartButtonOption, + AddChartButtonProps, + DefaultAddChartButtonValue, +} from '../../types/addChartButton'; +export type { + AddChartButtonOption, + AddChartButtonProps, + DefaultAddChartButtonValue, +} from '../../types/addChartButton'; +import {getChartTypeLabel} from '../../helpers/chartLabels'; import './AddChartButton.scss'; +import i18n from './i18n'; const block = cn('qp-add-chart-button'); -export type AddChartButtonOption = { - value: TValue; - text: ReactNode; - icon?: IconData; - disabled?: boolean; - hidden?: boolean; -}; - -export type DefaultAddChartButtonValue = - 'Area' | 'BarX' | 'BarY' | 'Line' | 'Pie' | 'Scatter' | 'Waterfall'; - export const DEFAULT_ADD_CHART_BUTTON_OPTIONS: readonly AddChartButtonOption[] = [ - {value: 'Area', text: 'Area', icon: ChartAreaStacked}, - {value: 'BarX', text: 'BarX', icon: ChartBar}, - {value: 'BarY', text: 'BarY', icon: ChartColumn}, - {value: 'Line', text: 'Line', icon: ChartLine}, - {value: 'Pie', text: 'Pie', icon: ChartPie}, - {value: 'Scatter', text: 'Scatter', icon: Circles5Random}, - {value: 'Waterfall', text: 'Waterfall', icon: SquareBars}, + { + value: 'Area', + get text() { + return getChartTypeLabel('area'); + }, + icon: ChartAreaStacked, + }, + { + value: 'BarX', + get text() { + return getChartTypeLabel('bar-y'); + }, + icon: ChartBar, + }, + { + value: 'BarY', + get text() { + return getChartTypeLabel('bar-x'); + }, + icon: ChartColumn, + }, + { + value: 'Line', + get text() { + return getChartTypeLabel('line'); + }, + icon: ChartLine, + }, + { + value: 'Pie', + get text() { + return getChartTypeLabel('pie'); + }, + icon: ChartPie, + }, + { + value: 'Scatter', + get text() { + return getChartTypeLabel('scatter'); + }, + icon: Circles5Random, + }, + { + value: 'Waterfall', + get text() { + return getChartTypeLabel('waterfall'); + }, + icon: SquareBars, + }, ]; -type SwitcherButtonProps = Omit< - ButtonButtonProps, - 'children' | 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'selected' | 'size' | 'view' ->; - -type MenuProps = NonNullable['menuProps']>; -type PopupProps = NonNullable['popupProps']>; - -export type AddChartButtonProps = { - text?: ReactNode; - options?: readonly AddChartButtonOption[]; - onSelect: (value: TValue) => void; - open?: boolean; - onOpenToggle?: (open: boolean) => void; - disabled?: boolean; - className?: string; - buttonProps?: SwitcherButtonProps; - menuProps?: MenuProps; - popupProps?: PopupProps; -}; - export const AddChartButton = ({ - text = 'Add chart', - options = DEFAULT_ADD_CHART_BUTTON_OPTIONS as readonly AddChartButtonOption[], + text = i18n('action_add-chart'), + options, onSelect, open, onOpenToggle, @@ -81,17 +96,15 @@ export const AddChartButton = - options.map(({value, text: optionText, icon, disabled: optionDisabled, hidden}) => ({ - text: optionText, - iconStart: icon && , - disabled: optionDisabled, - hidden, - action: () => onSelect(value), - })), - [onSelect, options], - ); + const items = ( + options ?? (DEFAULT_ADD_CHART_BUTTON_OPTIONS as readonly AddChartButtonOption[]) + ).map(({value, text: optionText, icon, disabled: optionDisabled, hidden}) => ({ + text: optionText, + iconStart: icon && , + disabled: optionDisabled, + hidden, + action: () => onSelect(value), + })); const handleOpenToggle: React.Dispatch> = (nextOpen) => { const nextValue = typeof nextOpen === 'function' ? nextOpen(isOpen) : nextOpen; diff --git a/src/components/AddChartButton/i18n/dicts.ts b/src/components/AddChartButton/i18n/dicts.ts new file mode 100644 index 0000000..99e66a4 --- /dev/null +++ b/src/components/AddChartButton/i18n/dicts.ts @@ -0,0 +1,3 @@ +import en from './en.json'; +import ru from './ru.json'; +export default {en, ru}; diff --git a/src/components/AddChartButton/i18n/en.json b/src/components/AddChartButton/i18n/en.json new file mode 100644 index 0000000..5a88a24 --- /dev/null +++ b/src/components/AddChartButton/i18n/en.json @@ -0,0 +1,3 @@ +{ + "action_add-chart": "Add chart" +} diff --git a/src/components/AddChartButton/i18n/index.ts b/src/components/AddChartButton/i18n/index.ts new file mode 100644 index 0000000..9395c81 --- /dev/null +++ b/src/components/AddChartButton/i18n/index.ts @@ -0,0 +1,4 @@ +import {addI18Keysets} from '../../../i18n'; +import dicts from './dicts'; +// Standalone add-chart control. +export default addI18Keysets('qp:add-chart-button', dicts); diff --git a/src/components/AddChartButton/i18n/ru.json b/src/components/AddChartButton/i18n/ru.json new file mode 100644 index 0000000..eb23579 --- /dev/null +++ b/src/components/AddChartButton/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "action_add-chart": "Добавить график" +} diff --git a/src/components/AttachmentList/AttachmentList.tsx b/src/components/AttachmentList/AttachmentList.tsx index d26c8e2..e006da3 100644 --- a/src/components/AttachmentList/AttachmentList.tsx +++ b/src/components/AttachmentList/AttachmentList.tsx @@ -1,27 +1,10 @@ -import React, {ReactNode, useMemo} from 'react'; -import {Flex, List, ListProps} from '@gravity-ui/uikit'; -import {AttachmentItem, AttachmentItemProps} from './internal/AttachmentItem/AttachmentItem'; -import {isInteractiveTarget} from './helpers/isInteractiveTarget'; +import {Flex, List} from '@gravity-ui/uikit'; +import React, {useMemo} from 'react'; +import type {AttachmentListProps} from '../../types/attachmentListProps'; import './AttachmentList.scss'; - -export type AttachmentListProps< - T extends AttachmentItemProps['attachment'] = AttachmentItemProps['attachment'], -> = Omit, 'items' | 'selectedItemIndex' | 'virtualized'> & { - attachments: AttachmentItemProps['attachment'][]; - className?: string; - onDelete?: (attachment: AttachmentItemProps['attachment']) => void; - onEdit?: (attachment: AttachmentItemProps['attachment']) => void; - onRevert?: (attachment: AttachmentItemProps['attachment']) => void; - - renderEditForm?: (attachment: AttachmentItemProps['attachment']) => ReactNode; - - wasAddedIds?: string[]; - wasEditedIds?: string[]; - - editingIds?: string[]; - - isDeleted?: boolean; -}; +import {isInteractiveTarget} from './helpers/isInteractiveTarget'; +import {AttachmentItem} from './internal/AttachmentItem/AttachmentItem'; +export type {AttachmentListProps} from '../../types/attachmentListProps'; export const AttachmentList = ({ attachments, diff --git a/src/components/AttachmentList/helpers/objectLinkValidator.ts b/src/components/AttachmentList/helpers/objectLinkValidator.ts index 80a27ac..bf3b71a 100644 --- a/src/components/AttachmentList/helpers/objectLinkValidator.ts +++ b/src/components/AttachmentList/helpers/objectLinkValidator.ts @@ -1,16 +1,6 @@ +import type {EditLinkErrors} from '../../../types/attachmentsEditor'; import {stringRequiredValidator} from './stringRequiredValidator'; -export type EditLinkErrors = Partial<{ - link: string; - token: string; - name: string; -}>; - -export type ObjectLinkValidator = ( - values: Record, - isTokenRequired?: boolean, -) => EditLinkErrors | null; - export const objectLinkValidator = (values: Record, isTokenRequired?: boolean) => { const patchErrors: Partial = {}; diff --git a/src/components/AttachmentList/helpers/stringRequiredValidator.ts b/src/components/AttachmentList/helpers/stringRequiredValidator.ts index 2314d17..c1f568f 100644 --- a/src/components/AttachmentList/helpers/stringRequiredValidator.ts +++ b/src/components/AttachmentList/helpers/stringRequiredValidator.ts @@ -1,9 +1,8 @@ +import type {Validator} from '../../../types/attachmentsEditor'; import i18n from '../i18n'; const DEFAULT_MAX_LENGTH = 256; -export type Validator = (value: unknown) => string | null; - export const stringRequiredValidator: Validator = ( value: unknown, maxLength: number = DEFAULT_MAX_LENGTH, diff --git a/src/components/AttachmentList/index.ts b/src/components/AttachmentList/index.ts index c700d6f..a86878f 100644 --- a/src/components/AttachmentList/index.ts +++ b/src/components/AttachmentList/index.ts @@ -1,4 +1,7 @@ export {AttachmentList} from './AttachmentList'; -export type {AttachmentListProps} from './AttachmentList'; +export type {AttachmentListProps} from '../../types/attachmentListProps'; export * from './internal/EditAttachmentItem'; export * from './internal/AttachmentItem'; +export type {EditLinkErrors, ObjectLinkValidator, Validator} from '../../types/attachmentsEditor'; + +export type {Attachment} from '../../types/attachments'; diff --git a/src/components/AttachmentList/internal/AttachmentItem/AttachmentItem.tsx b/src/components/AttachmentList/internal/AttachmentItem/AttachmentItem.tsx index e9b80eb..c9cd335 100644 --- a/src/components/AttachmentList/internal/AttachmentItem/AttachmentItem.tsx +++ b/src/components/AttachmentList/internal/AttachmentItem/AttachmentItem.tsx @@ -1,3 +1,4 @@ +import type {AttachmentItemProps} from '../../../../types/attachmentsEditor'; import React, {useMemo, useState} from 'react'; import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; import {ArrowRotateLeft, Link, Pencil, TrashBin} from '@gravity-ui/icons'; @@ -5,16 +6,6 @@ import {getAttachmentIcon} from '../../helpers/getIconByAttachmentName'; import cn from 'bem-cn-lite'; import './AttachmentItem.scss'; -export type AttachmentItemProps = { - attachment: {id: string; name: string; link?: string}; - wasEdited?: boolean; - wasAdded?: boolean; - onEdit?: (attachment: AttachmentItemProps['attachment']) => void; - onDelete?: (attachment: AttachmentItemProps['attachment']) => void; - onRevert?: (attachment: AttachmentItemProps['attachment']) => void; - isDeleted?: boolean; -}; - const block = cn('qp-attachment-item'); export const AttachmentItem = ({ diff --git a/src/components/AttachmentList/internal/AttachmentItem/index.ts b/src/components/AttachmentList/internal/AttachmentItem/index.ts index cdc133a..57bd899 100644 --- a/src/components/AttachmentList/internal/AttachmentItem/index.ts +++ b/src/components/AttachmentList/internal/AttachmentItem/index.ts @@ -1,2 +1,2 @@ export {AttachmentItem} from './AttachmentItem'; -export type {AttachmentItemProps} from './AttachmentItem'; +export type {AttachmentItemProps} from '../../../../types/attachmentsEditor'; diff --git a/src/components/AttachmentList/internal/EditAttachmentItem/EditAttachmentItem.tsx b/src/components/AttachmentList/internal/EditAttachmentItem/EditAttachmentItem.tsx index 23fe0f8..e59179c 100644 --- a/src/components/AttachmentList/internal/EditAttachmentItem/EditAttachmentItem.tsx +++ b/src/components/AttachmentList/internal/EditAttachmentItem/EditAttachmentItem.tsx @@ -1,16 +1,7 @@ +import type {EditAttachmentItemProps} from '../../../../types/attachmentsEditor'; import React from 'react'; import {FileEdit} from './FileEdit'; import {EditLinkItem} from './EditLinkItem'; -import type {EditFileItemProps} from './FileEdit'; -import type {EditLinkItemProps} from './EditLinkItem'; - -export type EditAttachmentItemProps = - | (EditFileItemProps & { - type: 'file'; - }) - | (EditLinkItemProps & { - type: 'link'; - }); export const EditAttachmentItem = ({...props}: EditAttachmentItemProps) => { if (props.type === 'file') { diff --git a/src/components/AttachmentList/internal/EditAttachmentItem/EditLinkItem.tsx b/src/components/AttachmentList/internal/EditAttachmentItem/EditLinkItem.tsx index d74ca01..5d7aa36 100644 --- a/src/components/AttachmentList/internal/EditAttachmentItem/EditLinkItem.tsx +++ b/src/components/AttachmentList/internal/EditAttachmentItem/EditLinkItem.tsx @@ -1,40 +1,20 @@ +import type { + EditLinkErrors, + EditLinkItemProps, + EditLinkValues, +} from '../../../../types/attachmentsEditor'; import React, {useId, useRef, useState} from 'react'; import {Box, Button, Flex, Icon, Select, Text, TextArea, TextInput} from '@gravity-ui/uikit'; import {Check, Xmark} from '@gravity-ui/icons'; import {useLabelRef} from '../../hooks/useLabelRef'; import {useKeyDownFormControl} from '../../hooks/useKeyDownFormControl'; -import {ObjectLinkValidator, objectLinkValidator} from '../../helpers/objectLinkValidator'; +import {objectLinkValidator} from '../../helpers/objectLinkValidator'; import cn from 'bem-cn-lite'; import i18n from '../../i18n'; import './EditLinkItem.scss'; -export type EditLinkValues = { - link: string; - token: string; - name: string; -}; - -type EditLinkErrors = { - link: string; - token: string; - name: string; -}; - -export type EditLinkItemProps = { - linkLabel?: string; - tokenLabel?: string; - nameLabel?: string; - onAccept?: (values: EditLinkValues) => void; - onCancel?: () => void; - onChange?: (pathedValues: EditLinkValues) => void; - tokens?: {value: string; title: string}[]; - values?: EditLinkValues; - defaultValues?: EditLinkValues; - validator?: ObjectLinkValidator; -}; - const block = cn('edit-link-item'); export const EditLinkItem = ({ diff --git a/src/components/AttachmentList/internal/EditAttachmentItem/FileEdit.tsx b/src/components/AttachmentList/internal/EditAttachmentItem/FileEdit.tsx index 9be9cfd..3d14d00 100644 --- a/src/components/AttachmentList/internal/EditAttachmentItem/FileEdit.tsx +++ b/src/components/AttachmentList/internal/EditAttachmentItem/FileEdit.tsx @@ -1,21 +1,12 @@ +import type {EditFileItemProps} from '../../../../types/attachmentsEditor'; import React, {useRef, useState} from 'react'; import {Button, Flex, Icon, TextInput} from '@gravity-ui/uikit'; import {Check, Xmark} from '@gravity-ui/icons'; import {useKeyDownFormControl} from '../../hooks/useKeyDownFormControl'; -import {Validator, stringRequiredValidator} from '../../helpers/stringRequiredValidator'; +import {stringRequiredValidator} from '../../helpers/stringRequiredValidator'; import i18n from '../../i18n'; -export type EditFileItemProps = { - fileName?: string; - fileLabel?: string; - defaultFileName?: string; - onChangeFileName?: (fileName: string) => void; - onAccept?: (newFileName: string) => void; - onCancel?: () => void; - validator?: Validator; -}; - export const FileEdit = ({ fileLabel: customerFileLabel, fileName: customerFileName, diff --git a/src/components/AttachmentList/internal/EditAttachmentItem/index.ts b/src/components/AttachmentList/internal/EditAttachmentItem/index.ts index 0b70d0e..3cb94bb 100644 --- a/src/components/AttachmentList/internal/EditAttachmentItem/index.ts +++ b/src/components/AttachmentList/internal/EditAttachmentItem/index.ts @@ -1,4 +1,4 @@ export {EditAttachmentItem} from './EditAttachmentItem'; -export type {EditAttachmentItemProps} from './EditAttachmentItem'; -export type {EditLinkItemProps, EditLinkValues} from './EditLinkItem'; -export type {EditFileItemProps} from './FileEdit'; +export type {EditAttachmentItemProps} from '../../../../types/attachmentsEditor'; +export type {EditLinkItemProps, EditLinkValues} from '../../../../types/attachmentsEditor'; +export type {EditFileItemProps} from '../../../../types/attachmentsEditor'; diff --git a/src/components/AttachmentListPlaceholder/AttachmentListPlaceholder.tsx b/src/components/AttachmentListPlaceholder/AttachmentListPlaceholder.tsx index fe7ad3c..f08a69d 100644 --- a/src/components/AttachmentListPlaceholder/AttachmentListPlaceholder.tsx +++ b/src/components/AttachmentListPlaceholder/AttachmentListPlaceholder.tsx @@ -1,21 +1,12 @@ -import React, {ReactNode} from 'react'; -import {Button, Flex, Icon, Link, Text} from '@gravity-ui/uikit'; -import {Folder} from '@gravity-ui/illustrations'; import {Plus} from '@gravity-ui/icons'; +import {Folder} from '@gravity-ui/illustrations'; +import {Button, Flex, Icon, Link, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import i18n from './i18n'; +import React from 'react'; +import type {AttachmentListPlaceholderProps} from '../../types/attachmentListPlaceholderProps'; import './AttachmentListPlaceholder.scss'; - -export type AttachmentListPlaceholderProps = { - title?: ReactNode; - description?: ReactNode; - linkForDoc?: string; - linkText?: string; - className?: string; - onAddFile?: () => void; - onAddLink?: () => void; - qa?: string; -}; +import i18n from './i18n'; +export type {AttachmentListPlaceholderProps} from '../../types/attachmentListPlaceholderProps'; const block = cn('qp-attachment-list-placeholder'); diff --git a/src/components/Breadcrumbs/Breadcrumbs.stories.tsx b/src/components/Breadcrumbs/Breadcrumbs.stories.tsx new file mode 100644 index 0000000..b0d3040 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.stories.tsx @@ -0,0 +1,43 @@ +import React, {useEffect, useState} from 'react'; +import type {Meta, StoryObj} from '@storybook/react'; +import {fn} from 'storybook/test'; +import {Breadcrumbs} from './Breadcrumbs'; + +const meta: Meta = { + title: 'Components/Breadcrumbs', + component: Breadcrumbs, + tags: ['autodocs'], + args: {location: {cluster: 'production', path: '//home/analytics/events'}, onUpdate: fn()}, + render: function Render(args) { + const [location, setLocation] = useState(args.location); + useEffect(() => setLocation(args.location), [args.location]); + return ( + { + setLocation(value); + args.onUpdate(value); + }} + /> + ); + }, +}; +export default meta; +type Story = StoryObj; +export const Default: Story = {}; +export const Empty: Story = {args: {location: {cluster: undefined, path: undefined}}}; +export const ClusterRoot: Story = {args: {location: {cluster: 'production', path: undefined}}}; +export const LongPath: Story = { + args: {location: {cluster: 'production', path: '//home/analytics/daily/2026/october/events'}}, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; +export const WithLinks: Story = { + args: {getBreadcrumbHref: ({cluster, path}) => `#${cluster ?? ''}${path ?? ''}`}, +}; diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx index 346ed18..60df2f8 100644 --- a/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -1,23 +1,14 @@ -import React, {FC, useState} from 'react'; -import {Button, Flex, Breadcrumbs as GravityBreadcrumbs, Icon, Text} from '@gravity-ui/uikit'; import FolderTreeIcon from '@gravity-ui/icons/svgs/folder-tree.svg'; import PencilIcon from '@gravity-ui/icons/svgs/pencil.svg'; +import {Button, Flex, Breadcrumbs as GravityBreadcrumbs, Icon, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import {parsePathSegments} from './helpers/parsePathSegments'; -import type {GetNavigationBreadcrumbHref, NavigationLocation} from '../../types/navigation'; -import type {LoadPathSuggestions} from '../../types/pathEditor'; +import React, {FC, useState} from 'react'; +import type {BreadcrumbsProps} from '../../types/breadcrumbsProps'; import {PathEditor} from '../PathEditor'; -import i18n from './i18n'; import './Breadcrumbs.scss'; - -export type BreadcrumbsProps = { - location: NavigationLocation; - hideResetButton?: boolean; - className?: string; - onUpdate: (location: NavigationLocation) => void; - getBreadcrumbHref?: GetNavigationBreadcrumbHref; - onLoadSuggestions?: LoadPathSuggestions; -}; +import {parsePathSegments} from './helpers/parsePathSegments'; +import i18n from './i18n'; +export type {BreadcrumbsProps} from '../../types/breadcrumbsProps'; const block = cn('qp-breadcrumbs'); diff --git a/src/components/Chart/Chart.tsx b/src/components/Chart/Chart.tsx index 9aa390c..7a7d781 100644 --- a/src/components/Chart/Chart.tsx +++ b/src/components/Chart/Chart.tsx @@ -1,12 +1,10 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react'; -import { - type ChartRef, - Chart as GravityChart, - type ChartProps as GravityChartProps, -} from '@gravity-ui/charts'; +import {type ChartRef, Chart as GravityChart} from '@gravity-ui/charts'; import {ChevronsCollapseUpRight, ChevronsExpandUpRight, Ellipsis, Pencil} from '@gravity-ui/icons'; import {Button, DropdownMenu, type DropdownMenuItem, Icon, Portal} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; +import type {ChartProps} from '../../types/chartProps'; +export type {ChartAction, ChartProps} from '../../types/chartProps'; import i18n from './i18n'; @@ -14,21 +12,6 @@ import './Chart.scss'; const block = cn('qp-chart'); -export type ChartAction = { - text: React.ReactNode; - icon?: React.ReactNode; - hidden?: boolean; - disabled?: boolean; - onClick: () => void; -}; - -export type ChartProps = GravityChartProps & { - actions?: readonly ChartAction[]; - onPencilEdit?: () => void; - controlsVisibility?: 'always' | 'hover'; - className?: string; -}; - export const Chart = React.forwardRef(function ChartComponent( {actions, onPencilEdit, controlsVisibility = 'always', className, ...chartProps}, ref, diff --git a/src/components/ClusterRow/ClusterRow.tsx b/src/components/ClusterRow/ClusterRow.tsx index d19e843..742347e 100644 --- a/src/components/ClusterRow/ClusterRow.tsx +++ b/src/components/ClusterRow/ClusterRow.tsx @@ -1,15 +1,12 @@ -import React, {FC} from 'react'; import {Avatar, Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {FC} from 'react'; +import type {ClusterRowProps} from '../../types/clusterRowProps'; import './ClusterRow.scss'; -import {NavigationCluster} from '../../types/navigation'; +export type {ClusterRowProps} from '../../types/clusterRowProps'; const block = cn('qp-cluster-row'); -export type ClusterRowProps = { - cluster: NavigationCluster; -}; - export const ClusterRow: FC = ({ cluster: {icon, title, color, backgroundColor, description}, }) => { diff --git a/src/components/Dashboard/Dashboard.stories.tsx b/src/components/Dashboard/Dashboard.stories.tsx index 29f130d..92f417f 100644 --- a/src/components/Dashboard/Dashboard.stories.tsx +++ b/src/components/Dashboard/Dashboard.stories.tsx @@ -6,7 +6,7 @@ import type {Meta, StoryObj} from '@storybook/react'; import {action} from 'storybook/actions'; import {Dashboard} from './Dashboard'; -import type {DashboardRenderItem} from './types'; +import type {DashboardRenderItem} from '../../types/dashboard'; import './Dashboard.stories.scss'; @@ -293,3 +293,28 @@ export const ControlledLayout: Story = {render: () => } export const CustomGrid: Story = {render: () => }; export const AddItems: Story = {render: () => }; + +function FocusEventsExample() { + const [items, setItems] = React.useState(baseItems); + const [event, setEvent] = React.useState('Focus a card to inspect its current index.'); + return ( + setItems([...items].reverse())}>Reverse items} + > + + setEvent(`Focused ${items[index].id}, index ${index}`) + } + onItemBlur={(_content, index) => + setEvent(`Blurred ${items[index].id}, index ${index}`) + } + /> + + ); +} + +export const FocusEvents: Story = {render: () => }; diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx index 4ca8d9d..573a976 100644 --- a/src/components/Dashboard/Dashboard.tsx +++ b/src/components/Dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {Config} from '@gravity-ui/dashkit'; -import type {DashboardProps} from './types'; +import type {DashboardProps} from '../../types/dashboard'; import {Flex} from '@gravity-ui/uikit'; import {DRAG_HANDLE_CLASSNAME, DashPlate} from './internal/DashPlate'; import {DashboardProvider} from './internal/DashboardProvider'; @@ -26,6 +26,8 @@ export const Dashboard = ({ focusable = false, className, onLayoutChange, + onItemFocus, + onItemBlur, }: DashboardProps) => { const {config, gridGroups} = useDashboardConfig(items, customerGridState, defaultLayout); @@ -44,6 +46,14 @@ export const Dashboard = ({ draggableHandleClassName={DRAG_HANDLE_CLASSNAME} focusable={focusable} onChange={handleChange} + onItemFocus={({id}) => { + const index = items.findIndex((item) => item.id === id); + if (index !== -1) onItemFocus?.(items[index].content, index); + }} + onItemBlur={({id}) => { + const index = items.findIndex((item) => item.id === id); + if (index !== -1) onItemBlur?.(items[index].content, index); + }} /> diff --git a/src/components/Dashboard/helpers/createDefaultLayout.ts b/src/components/Dashboard/helpers/createDefaultLayout.ts index 0e1b43a..4aab238 100644 --- a/src/components/Dashboard/helpers/createDefaultLayout.ts +++ b/src/components/Dashboard/helpers/createDefaultLayout.ts @@ -1,4 +1,4 @@ -import type {DashboardRenderItem} from '../types'; +import type {DashboardRenderItem} from '../../../types/dashboard'; import type {ConfigLayout} from '@gravity-ui/dashkit'; const DEFAULT_ITEM_HEIGHT = 4; diff --git a/src/components/Dashboard/hooks/useDashboardConfig.ts b/src/components/Dashboard/hooks/useDashboardConfig.ts index b8301ee..c005592 100644 --- a/src/components/Dashboard/hooks/useDashboardConfig.ts +++ b/src/components/Dashboard/hooks/useDashboardConfig.ts @@ -1,6 +1,6 @@ import {useMemo} from 'react'; import {createDefaultLayout} from '../helpers/createDefaultLayout'; -import type {DashboardProps, DashboardRenderItem} from '../types'; +import type {DashboardProps, DashboardRenderItem} from '../../../types/dashboard'; import type {Config, ConfigLayout, DashKitGroup, ReactGridLayoutProps} from '@gravity-ui/dashkit'; export const DASHBOARD_PLUGIN_TYPE = 'querieskit-dashboard-item'; diff --git a/src/components/Dashboard/index.ts b/src/components/Dashboard/index.ts index 8755132..ce99db2 100644 --- a/src/components/Dashboard/index.ts +++ b/src/components/Dashboard/index.ts @@ -1,2 +1,6 @@ export {Dashboard} from './Dashboard'; -export type {DashboardItemAppearance, DashboardProps} from './types'; +export type { + DashboardItemAppearance, + DashboardRenderItem, + DashboardProps, +} from '../../types/dashboard'; diff --git a/src/components/Dashboard/internal/DashboardProvider.tsx b/src/components/Dashboard/internal/DashboardProvider.tsx index 72924e8..fdb0041 100644 --- a/src/components/Dashboard/internal/DashboardProvider.tsx +++ b/src/components/Dashboard/internal/DashboardProvider.tsx @@ -1,5 +1,5 @@ import React, {createContext, useContext, useMemo} from 'react'; -import type {DashboardRenderItem} from '../types'; +import type {DashboardRenderItem} from '../../../types/dashboard'; type DashboardContextValue = { items: DashboardRenderItem[]; diff --git a/src/components/DataTable/DataTable.tsx b/src/components/DataTable/DataTable.tsx index a035cef..853d9b4 100644 --- a/src/components/DataTable/DataTable.tsx +++ b/src/components/DataTable/DataTable.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import BaseDataTable, { - DataTableProps as BaseDataTableProps, - Column, -} from '@gravity-ui/react-data-table'; +import BaseDataTable, {Column} from '@gravity-ui/react-data-table'; import {Skeleton} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React from 'react'; +import type {DataTableProps} from '../../types/dataTableProps'; +export type {DataTableProps} from '../../types/dataTableProps'; -import {EmptyContent, EmptyContentVariant} from '../EmptyContent'; +import {EmptyContent} from '../EmptyContent'; import './DataTable.scss'; @@ -16,13 +15,6 @@ const block = cn('qp-data-table'); const SKELETON_ROWS_COUNT = 4; -export type DataTableProps = { - loading?: boolean; - loaded?: boolean; - className?: string; - emptyVariant?: EmptyContentVariant; -} & Omit, 'theme'>; - function getSkeletonWidth(rowIndex: number, columnIndex: number) { const widths = [72, 120, 180, 96]; diff --git a/src/components/EditableRowTitle/EditableRowTitle.tsx b/src/components/EditableRowTitle/EditableRowTitle.tsx index ed1e277..72e2970 100644 --- a/src/components/EditableRowTitle/EditableRowTitle.tsx +++ b/src/components/EditableRowTitle/EditableRowTitle.tsx @@ -1,11 +1,8 @@ -import React, {KeyboardEvent, useEffect, useState} from 'react'; import {Text, TextInput} from '@gravity-ui/uikit'; -import {QueryListEditingRenderData, QueryListRow} from '../../types/queryList'; - -export type EditableRowTitleProps = { - item: T; - editing?: QueryListEditingRenderData; -}; +import React, {KeyboardEvent, useEffect, useState} from 'react'; +import type {EditableRowTitleProps} from '../../types/editableRowTitleProps'; +import {QueryListRow} from '../../types/queryList'; +export type {EditableRowTitleProps} from '../../types/editableRowTitleProps'; export const EditableRowTitle = ({ item, diff --git a/src/components/EmptyContent/EmptyContent.tsx b/src/components/EmptyContent/EmptyContent.tsx index add029f..52d53c8 100644 --- a/src/components/EmptyContent/EmptyContent.tsx +++ b/src/components/EmptyContent/EmptyContent.tsx @@ -1,20 +1,14 @@ -import React, {FC} from 'react'; -import {Flex, Text} from '@gravity-ui/uikit'; import {Folder, NoSearchResults} from '@gravity-ui/illustrations'; +import {Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import i18n from './i18n'; +import React, {FC} from 'react'; +import type {EmptyContentProps, EmptyContentVariant} from '../../types/emptyContentProps'; import './EmptyContent.scss'; -import type {EmptyContentTextOverrides} from '../../types/listPanel'; +import i18n from './i18n'; +export type {EmptyContentProps, EmptyContentVariant} from '../../types/emptyContentProps'; const block = cn('qp-empty-content'); -export type EmptyContentVariant = 'no-files' | 'no-clusters' | 'nothing-found' | 'no-data'; - -export type EmptyContentProps = EmptyContentTextOverrides & { - variant: EmptyContentVariant; - className?: string; -}; - type EmptyContentConfig = { icon: FC<{height?: number}>; title: string; diff --git a/src/components/FieldsSearchToolbar/FieldsSearchToolbar.tsx b/src/components/FieldsSearchToolbar/FieldsSearchToolbar.tsx index 3b0b09d..08a9ba7 100644 --- a/src/components/FieldsSearchToolbar/FieldsSearchToolbar.tsx +++ b/src/components/FieldsSearchToolbar/FieldsSearchToolbar.tsx @@ -1,17 +1,8 @@ import React from 'react'; -import {FieldsSelector, type FieldsSelectorOption} from '../FieldsSelector'; +import type {FieldsSearchToolbarProps} from '../../types/fieldsSearchToolbarProps'; +import {FieldsSelector} from '../FieldsSelector'; import {SearchWithButtons} from '../SearchWithButtons'; - -export type FieldsSearchToolbarProps = { - search?: string; - onSearchUpdate?: (value: string) => void; - searchPlaceholder?: string; - fields: FieldsSelectorOption[]; - visibleFields: K[]; - onVisibleFieldsChange: (value: K[]) => void; - hideFieldsSelector?: boolean; - className?: string; -}; +export type {FieldsSearchToolbarProps} from '../../types/fieldsSearchToolbarProps'; export function FieldsSearchToolbar({ search, diff --git a/src/components/FieldsSelector/FieldsSelector.tsx b/src/components/FieldsSelector/FieldsSelector.tsx index 59c5731..0785b95 100644 --- a/src/components/FieldsSelector/FieldsSelector.tsx +++ b/src/components/FieldsSelector/FieldsSelector.tsx @@ -1,24 +1,13 @@ -import React, {ReactNode, useState} from 'react'; -import {Button, type ButtonProps, Flex, Icon, Popup, Text} from '@gravity-ui/uikit'; -import GearIcon from '@gravity-ui/icons/svgs/gear.svg'; import CheckIcon from '@gravity-ui/icons/svgs/check.svg'; +import GearIcon from '@gravity-ui/icons/svgs/gear.svg'; +import {Button, Flex, Icon, Popup, Text} from '@gravity-ui/uikit'; +import cn from 'bem-cn-lite'; +import React, {useState} from 'react'; import {useToggle} from '../../helpers/useToggle'; -import i18n from './i18n'; +import type {FieldsSelectorProps} from '../../types/fieldsSelectorProps'; import './FieldsSelector.scss'; -import cn from 'bem-cn-lite'; - -export type FieldsSelectorOption = { - id: K; - title: ReactNode; -}; - -export type FieldsSelectorProps = { - fields: FieldsSelectorOption[]; - value: K[]; - onChange: (value: K[]) => void; - buttonLabel?: string; - buttonView?: ButtonProps['view']; -}; +import i18n from './i18n'; +export type {FieldsSelectorOption, FieldsSelectorProps} from '../../types/fieldsSelectorProps'; const block = cn('qp-fields-selector'); diff --git a/src/components/HistoryFilter/HistoryFilter.stories.tsx b/src/components/HistoryFilter/HistoryFilter.stories.tsx index 08706e7..cde4205 100644 --- a/src/components/HistoryFilter/HistoryFilter.stories.tsx +++ b/src/components/HistoryFilter/HistoryFilter.stories.tsx @@ -1,3 +1,5 @@ +import React from 'react'; +import {Button, Flex, Text} from '@gravity-ui/uikit'; import type {Meta, StoryObj} from '@storybook/react'; import {FormField} from '../SimpleForm'; import {HistoryFilter} from './HistoryFilter'; @@ -67,3 +69,28 @@ export const WithInitialValues: Story = { onReset: () => alert('Reset'), }, }; + +function ExternalUpdatesExample() { + const [values, setValues] = React.useState>({onlyMine: false}); + const [applied, setApplied] = React.useState>({}); + return ( + + setApplied({})} + /> + + + + + Applied: {JSON.stringify(applied)} + + ); +} + +/** Edit multiple fields, Apply or Reset, and replace the values externally. */ +export const ExternalUpdates: Story = {render: () => }; diff --git a/src/components/HistoryFilter/HistoryFilter.tsx b/src/components/HistoryFilter/HistoryFilter.tsx index 432f88f..af5def3 100644 --- a/src/components/HistoryFilter/HistoryFilter.tsx +++ b/src/components/HistoryFilter/HistoryFilter.tsx @@ -1,17 +1,19 @@ -import React, {FC, useRef, useState} from 'react'; +import React, {FC, useState} from 'react'; import {Button, Flex, Icon, Popup} from '@gravity-ui/uikit'; import FunnelIcon from '@gravity-ui/icons/svgs/funnel.svg'; import ArrowRotateLeftIcon from '@gravity-ui/icons/svgs/arrow-rotate-left.svg'; import {useToggle} from '../../helpers/useToggle'; -import type {QueryListFilterConfig} from '../../types/queryList'; +import type {HistoryFilterProps} from '../../types/historyFilter'; import cn from 'bem-cn-lite'; import {SimpleForm} from '../SimpleForm'; +import {getInitialValues} from '../SimpleForm/helpers/getInitialValues'; +import {equalFilterValues} from './helpers/equalFilterValues'; import i18n from './i18n'; import './HistoryFilter.scss'; const block = cn('qp-history-filter'); -export const HistoryFilter: FC = ({ +export const HistoryFilter: FC = ({ fields = [], values, initialValues, @@ -22,7 +24,16 @@ export const HistoryFilter: FC { const [buttonElement, setButtonElement] = useState(null); const [open, toggleOpen] = useToggle(false); - const valuesRef = useRef>(values ?? initialValues ?? {}); + const defaults = getInitialValues(fields, initialValues); + const [draft, setDraft] = useState(() => ({...defaults, ...values})); + const [previousValues, setPreviousValues] = useState(values); + + // Sync external changes during render, before the form receives stale draft values. + // Equivalent objects from a parent rerender must preserve unsubmitted edits. + if (!equalFilterValues(previousValues, values)) { + setPreviousValues(values); + setDraft({...defaults, ...values}); + } const normalView = isChanged ? 'action' : 'normal'; const flatView = isChanged ? 'flat-info' : 'flat'; @@ -45,20 +56,18 @@ export const HistoryFilter: FC
- { - valuesRef.current = nextValues; - }} - /> +
- - diff --git a/src/components/HistoryFilter/helpers/equalFilterValues.ts b/src/components/HistoryFilter/helpers/equalFilterValues.ts new file mode 100644 index 0000000..4c4de1b --- /dev/null +++ b/src/components/HistoryFilter/helpers/equalFilterValues.ts @@ -0,0 +1,22 @@ +/** Compare form data, including arrays and date values, independently of object key order. */ +export function equalFilterValues(left: unknown, right: unknown): boolean { + if (Object.is(left, right)) return true; + if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false; + if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false; + const leftValue = left.valueOf(); + const rightValue = right.valueOf(); + if (leftValue !== left || rightValue !== right) return Object.is(leftValue, rightValue); + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); + return ( + leftKeys.length === rightKeys.length && + leftKeys.every( + (key) => + Object.prototype.hasOwnProperty.call(right, key) && + equalFilterValues( + (left as Record)[key], + (right as Record)[key], + ), + ) + ); +} diff --git a/src/components/HistoryFilter/index.ts b/src/components/HistoryFilter/index.ts index 731d334..0be6708 100644 --- a/src/components/HistoryFilter/index.ts +++ b/src/components/HistoryFilter/index.ts @@ -1 +1,2 @@ export {HistoryFilter} from './HistoryFilter'; +export type {HistoryFilterProps} from '../../types/historyFilter'; diff --git a/src/components/HistoryGroupHeader/HistoryGroupHeader.tsx b/src/components/HistoryGroupHeader/HistoryGroupHeader.tsx index a04b608..67223f0 100644 --- a/src/components/HistoryGroupHeader/HistoryGroupHeader.tsx +++ b/src/components/HistoryGroupHeader/HistoryGroupHeader.tsx @@ -1,11 +1,9 @@ -import React, {FC} from 'react'; import {Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {FC} from 'react'; +import type {HistoryGroupHeaderProps as Props} from '../../types/historyGroupHeaderProps'; import './HistoryGroupHeader.scss'; - -type Props = { - title: string; -}; +export type {HistoryGroupHeaderProps as Props} from '../../types/historyGroupHeaderProps'; const block = cn('qp-history-list-header'); diff --git a/src/components/HistoryGroupHeader/index.ts b/src/components/HistoryGroupHeader/index.ts index ed1cf08..11578af 100644 --- a/src/components/HistoryGroupHeader/index.ts +++ b/src/components/HistoryGroupHeader/index.ts @@ -1 +1,3 @@ export {HistoryGroupHeader} from './HistoryGroupHeader'; + +export type {HistoryGroupHeaderProps} from '../../types/historyGroupHeaderProps'; diff --git a/src/components/HistoryPrivateIcon/HistoryPrivateIcon.tsx b/src/components/HistoryPrivateIcon/HistoryPrivateIcon.tsx index 5ac3285..0f002c5 100644 --- a/src/components/HistoryPrivateIcon/HistoryPrivateIcon.tsx +++ b/src/components/HistoryPrivateIcon/HistoryPrivateIcon.tsx @@ -1,13 +1,11 @@ -import React, {FC} from 'react'; -import {Icon} from '@gravity-ui/uikit'; -import LockIcon from '@gravity-ui/icons/svgs/lock.svg'; import LockOpenIcon from '@gravity-ui/icons/svgs/lock-open.svg'; -import './HistoryPrivateIcon.scss'; +import LockIcon from '@gravity-ui/icons/svgs/lock.svg'; +import {Icon} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; - -type Props = { - isPrivate?: boolean; -}; +import React, {FC} from 'react'; +import type {HistoryPrivateIconProps as Props} from '../../types/historyPrivateIconProps'; +import './HistoryPrivateIcon.scss'; +export type {HistoryPrivateIconProps as Props} from '../../types/historyPrivateIconProps'; const block = cn('qp-history-private-icon'); diff --git a/src/components/HistoryPrivateIcon/index.ts b/src/components/HistoryPrivateIcon/index.ts index 9339e13..7401df1 100644 --- a/src/components/HistoryPrivateIcon/index.ts +++ b/src/components/HistoryPrivateIcon/index.ts @@ -1 +1,3 @@ export {HistoryPrivateIcon} from './HistoryPrivateIcon'; + +export type {HistoryPrivateIconProps} from '../../types/historyPrivateIconProps'; diff --git a/src/components/LazyList/LazyList.tsx b/src/components/LazyList/LazyList.tsx index 8a3d8c0..7f342d3 100644 --- a/src/components/LazyList/LazyList.tsx +++ b/src/components/LazyList/LazyList.tsx @@ -1,9 +1,11 @@ -import React, {useLayoutEffect, useMemo, useRef} from 'react'; import {Flex, List, Loader} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {useLayoutEffect, useMemo, useRef} from 'react'; import {useLoadMoreSentinel} from '../../helpers/useLoadMoreSentinel'; +import type {LazyListProps} from '../../types/lazyListProps'; import {ListSpinner} from '../ListSpinner'; import './LazyList.scss'; +export type {LazyListProps} from '../../types/lazyListProps'; const block = cn('qp-lazy-list'); @@ -16,22 +18,6 @@ type LazyListRow = T | SentinelRow; const isSentinelRow = (row: LazyListRow): row is SentinelRow => Boolean(row) && typeof row === 'object' && '__sentinel' in (row as object); -export type LazyListProps = { - items: T[]; - itemHeight: (item: T) => number; - renderItem: (item: T, isActive: boolean, index: number) => React.ReactNode; - hasMore?: boolean; - onLoadMore?: () => void; - onItemClick?: (item: T, index: number) => void; - selectedItemIndex?: T[keyof T] | number; - filterable?: boolean; - loading?: boolean; - error?: React.ReactNode; - emptyContent?: React.ReactNode; - isEmpty?: boolean; - className?: string; -}; - export const LazyList = ({ items, itemHeight, diff --git a/src/components/ListSpinner/ListSpinner.tsx b/src/components/ListSpinner/ListSpinner.tsx index 1bffbd0..9113452 100644 --- a/src/components/ListSpinner/ListSpinner.tsx +++ b/src/components/ListSpinner/ListSpinner.tsx @@ -1,14 +1,12 @@ -import React, {FC} from 'react'; import {Flex, Spin} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {FC} from 'react'; +import type {ListSpinnerProps} from '../../types/listSpinnerProps'; import './ListSpinner.scss'; +export type {ListSpinnerProps} from '../../types/listSpinnerProps'; const block = cn('qp-list-spinner'); -export type ListSpinnerProps = { - className?: string; -}; - export const ListSpinner: FC = ({className}) => { return ( diff --git a/src/components/MonacoDiffEditor/MonacoDiffEditor.tsx b/src/components/MonacoDiffEditor/MonacoDiffEditor.tsx index 9164d5b..4d33a48 100644 --- a/src/components/MonacoDiffEditor/MonacoDiffEditor.tsx +++ b/src/components/MonacoDiffEditor/MonacoDiffEditor.tsx @@ -4,8 +4,7 @@ import cn from 'bem-cn-lite'; import type {editor} from 'monaco-editor'; // @ts-ignore Monaco's deep entrypoint does not expose its bundled declarations. import * as monaco from 'monaco-editor/editor/editor.api'; -import 'monaco-editor/languages/features/json/register'; -import '../MonacoEditor/monaco-yql-languages/monaco.contribution'; +import '../MonacoEditor/initialize'; import {MONACO_THEME_BY_UI} from '../MonacoEditor/MonacoEditorThemes'; import type {MonacoDiffEditorProps} from '../../types/monacoDiffEditor'; import {restoreHoverDelegateFactory} from './helpers/restoreHoverDelegateFactory'; diff --git a/src/components/MonacoEditor/MonacoEditor.tsx b/src/components/MonacoEditor/MonacoEditor.tsx index cfb949a..a1b5be8 100644 --- a/src/components/MonacoEditor/MonacoEditor.tsx +++ b/src/components/MonacoEditor/MonacoEditor.tsx @@ -5,8 +5,7 @@ import type {editor} from 'monaco-editor'; // @ts-ignore monaco-editor ships its own types but the default resolution // doesn't pick them up for this deep import path in this project setup. import * as monaco from 'monaco-editor/editor/editor.api'; -import 'monaco-editor/languages/features/json/register'; -import './monaco-yql-languages/monaco.contribution'; +import './initialize'; import {MONACO_THEME_BY_UI, MonacoThemeName, YT_LIGHT_MONACO_THEME} from './MonacoEditorThemes'; import type {MonacoEditorProps} from '../../types/monacoEditor'; import type {EditorExtension} from '../../types/editorProviders'; diff --git a/src/components/MonacoEditor/initialize.ts b/src/components/MonacoEditor/initialize.ts new file mode 100644 index 0000000..56179a4 --- /dev/null +++ b/src/components/MonacoEditor/initialize.ts @@ -0,0 +1,80 @@ +// Preserve the editor features of Monaco 0.56 without importing its root entrypoint, +// which also registers every bundled language and exports the LSP client. +// Keep this list aligned with editor.main.js when upgrading Monaco. +import 'monaco-editor/editor/contrib/anchorSelect/browser/anchorSelect'; +import 'monaco-editor/editor/contrib/bracketMatching/browser/bracketMatching'; +import 'monaco-editor/editor/contrib/caretOperations/browser/transpose'; +import 'monaco-editor/editor/contrib/clipboard/browser/clipboard'; +import 'monaco-editor/editor/contrib/codeAction/browser/codeActionContributions'; +import 'monaco-editor/editor/browser/widget/codeEditor/codeEditorWidget'; +import 'monaco-editor/editor/contrib/codelens/browser/codelensController'; +import 'monaco-editor/features/codicon/register'; +import 'monaco-editor/editor/contrib/colorPicker/browser/colorPickerContribution'; +import 'monaco-editor/editor/contrib/comment/browser/comment'; +import 'monaco-editor/editor/contrib/contextmenu/browser/contextmenu'; +import 'monaco-editor/editor/contrib/cursorUndo/browser/cursorUndo'; +import 'monaco-editor/editor/browser/widget/diffEditor/diffEditor.contribution'; +import 'monaco-editor/editor/contrib/diffEditorBreadcrumbs/browser/contribution'; +import 'monaco-editor/editor/contrib/dnd/browser/dnd'; +import 'monaco-editor/editor/contrib/documentSymbols/browser/documentSymbols'; +import 'monaco-editor/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution'; +import 'monaco-editor/features/find/register'; +import 'monaco-editor/editor/contrib/floatingMenu/browser/floatingMenu.contribution'; +import 'monaco-editor/editor/contrib/folding/browser/folding'; +import 'monaco-editor/editor/contrib/fontZoom/browser/fontZoom'; +import 'monaco-editor/editor/contrib/format/browser/formatActions'; +import 'monaco-editor/editor/contrib/gotoError/browser/gotoError'; +import 'monaco-editor/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess'; +import 'monaco-editor/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition'; +import 'monaco-editor/editor/contrib/gpu/browser/gpuActions'; +import 'monaco-editor/editor/contrib/hover/browser/hoverContribution'; +import 'monaco-editor/editor/contrib/indentation/browser/indentation'; +import 'monaco-editor/editor/contrib/inlayHints/browser/inlayHintsContribution'; +import 'monaco-editor/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution'; +import 'monaco-editor/editor/contrib/inlineProgress/browser/inlineProgress'; +import 'monaco-editor/editor/contrib/inPlaceReplace/browser/inPlaceReplace'; +import 'monaco-editor/editor/contrib/insertFinalNewLine/browser/insertFinalNewLine'; +import 'monaco-editor/editor/standalone/browser/inspectTokens/inspectTokens'; +import 'monaco-editor/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard'; +import 'monaco-editor/editor/contrib/lineSelection/browser/lineSelection'; +import 'monaco-editor/editor/contrib/linesOperations/browser/linesOperations'; +import 'monaco-editor/editor/contrib/linkedEditing/browser/linkedEditing'; +import 'monaco-editor/editor/contrib/links/browser/links'; +import 'monaco-editor/editor/contrib/longLinesHelper/browser/longLinesHelper'; +import 'monaco-editor/editor/contrib/middleScroll/browser/middleScroll.contribution'; +import 'monaco-editor/editor/contrib/multicursor/browser/multicursor'; +import 'monaco-editor/editor/contrib/parameterHints/browser/parameterHints'; +import 'monaco-editor/editor/contrib/placeholderText/browser/placeholderText.contribution'; +import 'monaco-editor/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess'; +import 'monaco-editor/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess'; +import 'monaco-editor/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess'; +import 'monaco-editor/editor/contrib/readOnlyMessage/browser/contribution'; +import 'monaco-editor/editor/standalone/browser/referenceSearch/standaloneReferenceSearch'; +import 'monaco-editor/editor/contrib/rename/browser/rename'; +import 'monaco-editor/editor/contrib/sectionHeaders/browser/sectionHeaders'; +import 'monaco-editor/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; +import 'monaco-editor/editor/contrib/smartSelect/browser/smartSelect'; +import 'monaco-editor/editor/contrib/snippet/browser/snippetController2'; +import 'monaco-editor/editor/contrib/stickyScroll/browser/stickyScrollContribution'; +import 'monaco-editor/editor/contrib/suggest/browser/suggestInlineCompletions'; +import 'monaco-editor/editor/standalone/browser/toggleHighContrast/toggleHighContrast'; +import 'monaco-editor/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; +import 'monaco-editor/editor/contrib/tokenization/browser/tokenization'; +import 'monaco-editor/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter'; +import 'monaco-editor/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators'; +import 'monaco-editor/editor/contrib/wordHighlighter/browser/wordHighlighter'; +import 'monaco-editor/editor/contrib/wordOperations/browser/wordOperations'; +import 'monaco-editor/editor/contrib/wordPartOperations/browser/wordPartOperations'; +import 'monaco-editor/editor/browser/coreCommands'; +import 'monaco-editor/editor/contrib/caretOperations/browser/caretOperations'; +import 'monaco-editor/editor/contrib/dropOrPasteInto/browser/copyPasteContribution'; +import 'monaco-editor/editor/contrib/find/browser/findController'; +import 'monaco-editor/editor/contrib/gotoSymbol/browser/goToCommands'; +import 'monaco-editor/editor/contrib/gotoError/browser/markerSelectionStatus'; +import 'monaco-editor/editor/contrib/semanticTokens/browser/documentSemanticTokens'; +import 'monaco-editor/editor/contrib/suggest/browser/suggestController'; +import 'monaco-editor/editor/common/standaloneStrings'; +// codicon-modifiers.css is loaded by the suggest/quick-access contributions above. +// Monaco's exports map does not expose CSS files directly. +import 'monaco-editor/languages/features/json/register'; +import './monaco-yql-languages/monaco.contribution'; diff --git a/src/components/MonacoEditor/monaco-yql-languages/_.contribution.ts b/src/components/MonacoEditor/monaco-yql-languages/_.contribution.ts index 0d81243..c14e8f4 100644 --- a/src/components/MonacoEditor/monaco-yql-languages/_.contribution.ts +++ b/src/components/MonacoEditor/monaco-yql-languages/_.contribution.ts @@ -1,12 +1,16 @@ -import {Emitter, type IEvent, languages} from 'monaco-editor'; +import type {IEvent, languages as MonacoLanguages} from 'monaco-editor'; +// @ts-ignore Monaco's deep entrypoint does not expose its bundled declarations. +import * as monaco from 'monaco-editor/editor/editor.api'; -interface ILang extends languages.ILanguageExtensionPoint { +const {Emitter, languages} = monaco as typeof import('monaco-editor'); + +interface ILang extends MonacoLanguages.ILanguageExtensionPoint { loader: () => Promise; } interface ILangImpl { - conf: languages.LanguageConfiguration; - language: languages.IMonarchLanguage; + conf: MonacoLanguages.LanguageConfiguration; + language: MonacoLanguages.IMonarchLanguage; } const languageDefinitions: {[languageId: string]: ILang} = {}; diff --git a/src/components/MonacoEditor/monaco-yql-languages/clickhouse/clickhouse.ts b/src/components/MonacoEditor/monaco-yql-languages/clickhouse/clickhouse.ts index e6551e3..f325064 100644 --- a/src/components/MonacoEditor/monaco-yql-languages/clickhouse/clickhouse.ts +++ b/src/components/MonacoEditor/monaco-yql-languages/clickhouse/clickhouse.ts @@ -203,6 +203,8 @@ function caseInsensitiveRegExp(pattern: string) { } function getCaseInsensitiveRegExp(patterns: string[]) { + // Empty alternatives must never match: a zero-width token cannot advance Monarch. + if (patterns.length === 0) return '(?!)'; return `(${patterns .map((pattern) => caseInsensitiveRegExp(pattern).replace(/\s+/g, '\\s')) .join('|')})`; diff --git a/src/components/NavigationActionButtons/NavigationActionButtons.tsx b/src/components/NavigationActionButtons/NavigationActionButtons.tsx index 6b7dfff..d33ff20 100644 --- a/src/components/NavigationActionButtons/NavigationActionButtons.tsx +++ b/src/components/NavigationActionButtons/NavigationActionButtons.tsx @@ -1,14 +1,7 @@ +import {Button, Flex} from '@gravity-ui/uikit'; import React from 'react'; -import {Button, type ButtonProps, Flex} from '@gravity-ui/uikit'; -import type {NavigationAction} from '../../types/navigation'; - -export type NavigationActionButtonsProps = { - actions?: Array>; - arg: TArg; - className?: string; - buttonClassName?: string; - size?: ButtonProps['size']; -}; +import type {NavigationActionButtonsProps} from '../../types/navigationActionButtonsProps'; +export type {NavigationActionButtonsProps} from '../../types/navigationActionButtonsProps'; export function NavigationActionButtons({ actions, diff --git a/src/components/NavigationItemRow/NavigationItemRow.tsx b/src/components/NavigationItemRow/NavigationItemRow.tsx index 6e54929..b8f3acc 100644 --- a/src/components/NavigationItemRow/NavigationItemRow.tsx +++ b/src/components/NavigationItemRow/NavigationItemRow.tsx @@ -1,16 +1,13 @@ -import React, {FC} from 'react'; import {Flex, Icon, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import './NavigationItemRow.scss'; -import {NavigationItem} from '../../types/navigation'; +import React, {FC} from 'react'; import {getDefaultNavigationIcon} from '../../helpers/getDefaultNavigationIcon'; +import type {NavigationItemRowProps} from '../../types/navigationItemRowProps'; +import './NavigationItemRow.scss'; +export type {NavigationItemRowProps} from '../../types/navigationItemRowProps'; const block = cn('qp-navigation-item-row'); -export type NavigationItemRowProps = { - item: NavigationItem; -}; - export const NavigationItemRow: FC = ({item}) => { return ( diff --git a/src/components/PathEditor/PathEditor.tsx b/src/components/PathEditor/PathEditor.tsx index c92a93d..281f54b 100644 --- a/src/components/PathEditor/PathEditor.tsx +++ b/src/components/PathEditor/PathEditor.tsx @@ -1,3 +1,5 @@ +import {Icon, Popup, Text, TextInput} from '@gravity-ui/uikit'; +import cn from 'bem-cn-lite'; import React, { FC, type FocusEvent, @@ -10,15 +12,9 @@ import React, { useRef, useState, } from 'react'; -import {Icon, Popup, Text, TextInput} from '@gravity-ui/uikit'; -import cn from 'bem-cn-lite'; -import type { - LoadPathSuggestions, - PathEditorEventPayload, - PathEditorSuggestion, - PathEditorSuggestionFilter, -} from '../../types/pathEditor'; import {getDefaultNavigationIcon} from '../../helpers/getDefaultNavigationIcon'; +import type {PathEditorSuggestion} from '../../types/pathEditor'; +import type {PathEditorProps} from '../../types/pathEditorProps'; import { filterByCurrentPath, getCompletedPath, @@ -28,31 +24,11 @@ import { } from './helpers/suggestions'; import i18n from './i18n'; import './PathEditor.scss'; +export type {PathEditorProps} from '../../types/pathEditorProps'; const DEBOUNCE_MS = 300; const block = cn('qp-path-editor'); -export type PathEditorProps = { - className?: string; - placeholder?: string; - defaultPath?: string; - disabled?: boolean; - autoFocus?: boolean; - hasClear?: boolean; - showErrors?: boolean; - customFilter?: PathEditorSuggestionFilter; - cluster?: string; - suggestions?: PathEditorSuggestion[]; - suggestionsError?: boolean; - errorMessage?: string; - onLoadSuggestions?: LoadPathSuggestions; - onChange?: (path: string) => void; - onFocus?: (event: FocusEvent, payload: PathEditorEventPayload) => void; - onBlur?: (path: string) => void; - onApply?: (path: string) => void; - onCancel?: () => void; -}; - export const PathEditor: FC = ({ className, placeholder = i18n('field_placeholder'), diff --git a/src/components/QueryDuration/QueryDuration.tsx b/src/components/QueryDuration/QueryDuration.tsx index ae73bdd..8047cfe 100644 --- a/src/components/QueryDuration/QueryDuration.tsx +++ b/src/components/QueryDuration/QueryDuration.tsx @@ -1,16 +1,10 @@ -import React, {FC} from 'react'; -import {QueryStatus} from '../../types/history'; import {Label} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {FC} from 'react'; +import type {QueryDurationProps as Props} from '../../types/queryDurationProps'; import './QueryDuration.scss'; import {useQueryDuration} from './useQueryDuration'; - -type Props = { - startTime?: string | number; - endTime?: string | number; - status: QueryStatus; - className?: string; -}; +export type {QueryDurationProps as Props} from '../../types/queryDurationProps'; const block = cn('qp-query-duration'); diff --git a/src/components/QueryDuration/index.ts b/src/components/QueryDuration/index.ts index 9b38ccc..adb8ef4 100644 --- a/src/components/QueryDuration/index.ts +++ b/src/components/QueryDuration/index.ts @@ -1 +1,3 @@ export {QueryDuration} from './QueryDuration'; + +export type {QueryDurationProps} from '../../types/queryDurationProps'; diff --git a/src/components/QueryGraph/QueryGraph.scss b/src/components/QueryGraph/QueryGraph.scss index a9b24df..b10ba6f 100644 --- a/src/components/QueryGraph/QueryGraph.scss +++ b/src/components/QueryGraph/QueryGraph.scss @@ -1,93 +1,4 @@ .qp-query-graph { - position: relative; - min-height: 360px; - height: 100%; - overflow: hidden; - background: var(--g-color-base-background); - - &__canvas { - width: 100%; - height: 100%; - min-height: 360px; - } - - &__toolbox { - position: absolute; - z-index: 4; - top: var(--g-spacing-4); - left: var(--g-spacing-4); - display: flex; - width: 28px; - flex-direction: column; - } - - &__toolbox-button { - &_position_top { - --g-button-border-radius: var(--g-border-radius-s) var(--g-border-radius-s) 0 0; - } - - &_position_bottom { - --g-button-border-radius: 0 0 var(--g-border-radius-s) var(--g-border-radius-s); - } - } - - &__hover-anchor { - position: absolute; - width: 1px; - height: 1px; - pointer-events: none; - } - - &__popup { - position: absolute; - z-index: 5; - width: min(440px, calc(100% - var(--g-spacing-8))); - max-height: calc(100% - var(--g-spacing-8)); - overflow: auto; - padding: var(--g-spacing-4); - border: 1px solid var(--g-color-line-generic); - border-radius: var(--g-border-radius-l); - box-shadow: 0 4px 24px var(--g-color-sfx-shadow); - background: var(--g-color-base-float); - } - - &__popup-content, - &__jobs, - &__schemas, - &__details { - display: flex; - flex-direction: column; - gap: var(--g-spacing-3); - } - - &__popup-title { - display: flex; - align-items: center; - gap: var(--g-spacing-2); - - > div { - display: flex; - min-width: 0; - flex-direction: column; - } - } - - &__detail-row { - display: flex; - justify-content: space-between; - gap: var(--g-spacing-3); - } - - &__state { - height: 100%; - min-height: 360px; - display: flex; - align-items: center; - justify-content: center; - padding: var(--g-spacing-6); - text-align: center; - } - &__large-graph { display: flex; max-width: 480px; diff --git a/src/components/QueryGraph/QueryGraph.stories.tsx b/src/components/QueryGraph/QueryGraph.stories.tsx index 5c8da71..b6ba0a0 100644 --- a/src/components/QueryGraph/QueryGraph.stories.tsx +++ b/src/components/QueryGraph/QueryGraph.stories.tsx @@ -1,66 +1,11 @@ import React, {useEffect, useState} from 'react'; import type {Meta, StoryObj} from '@storybook/react'; -import type {QueryGraphEdge, QueryGraphNode} from '../../types/queryGraph'; +import type {QueryGraphNode} from '../../types/queryGraph'; import {QueryGraph} from './QueryGraph'; import {largeGraphEdges, largeGraphNodes} from './story/largeGraphData'; -export const demoNodes: QueryGraphNode[] = [ - {id: 'source', kind: 'input', name: 'orders', label: 'warehouse.orders'}, - { - id: 'read', - kind: 'operation', - name: 'Read', - operationType: 'read', - status: 'completed', - progress: {total: 12, completed: 12}, - }, - { - id: 'map', - kind: 'operation', - name: 'Map', - operationType: 'map', - status: 'running', - progress: {total: 100, completed: 68, running: 8, pending: 24}, - popup: { - stages: [ - {name: 'Preparing', duration: 1300}, - {name: 'Executing', duration: 8400}, - ], - details: [ - {name: 'Columns', value: ['id', 'price', 'created_at']}, - {name: 'Settings', children: [{name: 'Workers', value: 8}]}, - ], - jobs: {total: 100, completed: 68, running: 8, pending: 24}, - inputs: [ - { - name: 'orders', - columns: [ - {name: 'id', type: 'Uint64'}, - {name: 'price', type: 'Double'}, - ], - }, - ], - outputs: [{name: 'mapped', columns: [{name: 'total', type: 'Double'}]}], - }, - }, - { - id: 'reduce', - kind: 'operation', - name: 'Reduce', - operationType: 'reduce', - status: 'waiting', - progress: {total: 20}, - }, - {id: 'sink', kind: 'output', name: 'result', label: 'warehouse.result'}, -]; - -export const demoEdges: QueryGraphEdge[] = [ - {id: 'source-read', source: 'source', target: 'read'}, - {id: 'read-map', source: 'read', target: 'map'}, - {id: 'map-reduce', source: 'map', target: 'reduce'}, - {id: 'reduce-sink', source: 'reduce', target: 'sink'}, -]; +import {demoEdges, demoNodes} from './story/demoData'; const meta: Meta = { title: 'Components/QueryGraph', diff --git a/src/components/QueryGraph/QueryGraph.tsx b/src/components/QueryGraph/QueryGraph.tsx index b5a9e4e..8dc01db 100644 --- a/src/components/QueryGraph/QueryGraph.tsx +++ b/src/components/QueryGraph/QueryGraph.tsx @@ -1,31 +1,13 @@ -import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {ECanDrag, GraphState, type TMultipointConnection} from '@gravity-ui/graph'; -import {GraphCanvas, useGraph, useGraphEvent} from '@gravity-ui/graph/react'; -import {MagnifierMinus, MagnifierPlus, SquareDashed} from '@gravity-ui/icons'; -import {Button, Icon, Loader, Text, Tooltip, useThemeValue} from '@gravity-ui/uikit'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; +import {Button, Loader, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; - -import type {QueryGraphNode, QueryGraphProps} from '../../types/queryGraph'; -import { - createQueryGraphConnections, - getQueryGraphNodeSize, - validateQueryGraph, -} from './helpers/layout'; +import type {QueryGraphProps} from '../../types/queryGraph'; +import {validateQueryGraph} from './helpers/layout'; +import {QueryGraphWithLayout} from './internal/QueryGraphWithLayout'; +import {QueryGraphState as State} from './internal/QueryGraphState'; import i18n from './i18n'; -import {type QueryGraphBlock, QueryGraphCanvasBlock} from './internal/QueryGraphCanvasBlock'; -import {QueryGraphConnection} from './internal/QueryGraphConnection'; -import {QueryGraphPopup} from './internal/QueryGraphPopup'; -import {getQueryGraphNodeIconSvgs} from './internal/queryGraphIcons'; -import {useQueryGraphLayout} from './internal/useQueryGraphLayout'; - import './QueryGraph.scss'; - const block = cn('qp-query-graph'); -const FIT_PADDING = 120; -const ZOOM_STEP = 0.08; -const renderEmptyBlock = () => ; - -type PopupState = {nodeId: string; left: number; top: number}; export function QueryGraph({ nodes, @@ -122,418 +104,3 @@ export function QueryGraph({ /> ); } - -type QueryGraphWithLayoutProps = Pick< - QueryGraphProps, - | 'nodes' - | 'edges' - | 'autoCenter' - | 'active' - | 'renderNodePopup' - | 'onNodeClick' - | 'onError' - | 'className' -> & {structureKey: string}; - -function QueryGraphWithLayout({ - nodes, - edges, - structureKey, - autoCenter, - active, - renderNodePopup, - onNodeClick, - onError, - className, -}: QueryGraphWithLayoutProps) { - const [popup, setPopup] = useState(); - const rootRef = useRef(null); - const nodesRef = useRef(nodes); - const edgesRef = useRef(edges); - nodesRef.current = nodes; - edgesRef.current = edges; - - const layoutBlocks = useMemo( - () => - nodesRef.current.map((node) => ({ - id: node.id, - ...getQueryGraphNodeSize(node), - })), - // Node status and presentation do not affect layout. - // eslint-disable-next-line react-hooks/exhaustive-deps - [structureKey], - ); - const layoutConnections = useMemo( - () => - edgesRef.current.map((edge) => ({ - id: edge.id, - sourceBlockId: edge.source, - targetBlockId: edge.target, - })), - // Only graph structure affects layout. - // eslint-disable-next-line react-hooks/exhaustive-deps - [structureKey], - ); - const {result, resolvedStructureKey, isInitialLoading, error} = useQueryGraphLayout({ - structureKey, - blocks: layoutBlocks, - connections: layoutConnections, - onError, - }); - - const graphBlocks = useMemo( - () => - nodes.flatMap((node) => { - const position = result?.blocks[node.id]; - if (!position) return []; - const {iconSvg, fallbackIconSvg, iconColorToken, fallbackIconColorToken} = - getQueryGraphNodeIconSvgs(node); - return [ - { - id: node.id, - is: 'query-node', - name: node.name, - selected: false, - anchors: [], - meta: { - node, - iconSvg, - fallbackIconSvg, - iconColorToken, - fallbackIconColorToken, - }, - ...getQueryGraphNodeSize(node), - ...position, - }, - ]; - }), - [nodes, result], - ); - const graphConnections = useMemo( - () => createQueryGraphConnections(edges, result?.edges), - [edges, result], - ); - - if (error) - return ( - - {error.message} - - ); - if (isInitialLoading || !result || !resolvedStructureKey) - return ( - - - - ); - - return ( - // The renderer is declared below to keep the public component and its states together. - // eslint-disable-next-line @typescript-eslint/no-use-before-define - - ); -} - -type RendererProps = Pick< - QueryGraphProps, - 'autoCenter' | 'active' | 'renderNodePopup' | 'onNodeClick' | 'className' -> & { - blocks: QueryGraphBlock[]; - connections: TMultipointConnection[]; - nodes: QueryGraphNode[]; - structureKey: string; - popup?: PopupState; - setPopup: React.Dispatch>; -}; - -const QueryGraphRenderer = React.forwardRef( - function QueryGraphRendererComponent( - { - blocks, - connections, - nodes, - structureKey, - autoCenter, - active, - popup, - setPopup, - renderNodePopup, - onNodeClick, - className, - }, - forwardedRef, - ) { - const rootRef = useRef(null); - useImperativeHandle(forwardedRef, () => rootRef.current as HTMLDivElement); - const attachedRef = useRef(false); - const centeredRef = useRef(false); - const previousActiveRef = useRef(false); - const previousStructureRef = useRef(structureKey); - const [attached, setAttached] = useState(false); - const [hoveredNodeId, setHoveredNodeId] = useState(); - const [hoverAnchor, setHoverAnchor] = useState<{left: number; top: number}>(); - const themeValue = useThemeValue(); - const {graph, api, setEntities, start, stop} = useGraph({ - name: 'query-graph', - settings: { - connection: QueryGraphConnection, - canDrag: ECanDrag.NONE, - canDragCamera: true, - canZoomCamera: true, - canCreateNewConnections: false, - useBezierConnections: true, - bezierConnectionDirection: 'horizontal', - showConnectionArrows: true, - useBlocksAnchors: false, - blockComponents: {'query-node': QueryGraphCanvasBlock}, - }, - }); - const [scale, setScale] = useState(graph.cameraService.getCameraState().scale); - const {scaleMin, scaleMax} = graph.cameraService.getCameraState(); - useGraphEvent(graph, 'camera-change', ({scale: nextScale}) => { - setScale(nextScale); - setHoverAnchor(undefined); - setHoveredNodeId(undefined); - }); - useGraphEvent(graph, 'mouseenter', ({target, sourceEvent}) => { - const state = (target as {state?: QueryGraphBlock} | undefined)?.state; - if (!state?.meta?.node) return; - setHoveredNodeId(state.meta.node.id); - // GraphLayer wraps synthetic mouseenter events around the original pointer event. - const mouseEvent = - sourceEvent instanceof CustomEvent ? sourceEvent.detail?.sourceEvent : sourceEvent; - if (rootRef.current && mouseEvent instanceof MouseEvent) { - const rect = rootRef.current.getBoundingClientRect(); - setHoverAnchor({ - left: mouseEvent.clientX - rect.left, - top: mouseEvent.clientY - rect.top, - }); - } - }); - useGraphEvent(graph, 'mouseleave', ({target}) => { - const state = (target as {state?: QueryGraphBlock} | undefined)?.state; - if (state?.meta?.node) { - setHoveredNodeId(undefined); - setHoverAnchor(undefined); - } - }); - - useEffect(() => { - if (!rootRef.current) return; - - const background = getComputedStyle(rootRef.current) - .getPropertyValue('--g-color-base-background') - .trim(); - const connection = getComputedStyle(rootRef.current) - .getPropertyValue('--g-color-line-generic') - .trim(); - if (!background || !connection) return; - - const colors = api.getGraphColors(); - api.updateGraphColors({ - ...colors, - canvas: { - ...colors.canvas, - belowLayerBackground: background, - layerBackground: background, - border: background, - dots: background, - }, - connection: { - ...colors.connection, - background: connection, - }, - }); - }, [api, themeValue]); - - const visibleConnections = useMemo( - () => - connections.map((connection) => ({ - ...connection, - selected: Boolean( - hoveredNodeId && - (connection.sourceBlockId === hoveredNodeId || - connection.targetBlockId === hoveredNodeId), - ), - })), - [connections, hoveredNodeId], - ); - useEffect( - () => setEntities({blocks, connections: visibleConnections}), - [blocks, setEntities, visibleConnections], - ); - useEffect(() => { - if (!attachedRef.current) return; - if (active) start(); - else stop(); - }, [active, start, stop]); - useEffect(() => { - const changed = previousStructureRef.current !== structureKey; - const becameActive = active && !previousActiveRef.current; - let frame: number | undefined; - if ( - attached && - active && - (!centeredRef.current || becameActive || (autoCenter && changed)) - ) { - frame = requestAnimationFrame(() => { - api.zoomToViewPort({padding: FIT_PADDING}); - centeredRef.current = true; - }); - } - previousActiveRef.current = Boolean(active); - previousStructureRef.current = structureKey; - setPopup(undefined); - setHoverAnchor(undefined); - setHoveredNodeId(undefined); - return () => { - if (frame !== undefined) cancelAnimationFrame(frame); - }; - }, [active, api, attached, autoCenter, setPopup, structureKey]); - - const handleClick: NonNullable['click']> = ({ - target, - sourceEvent, - }) => { - setHoverAnchor(undefined); - const state = (target as {state?: QueryGraphBlock; isBlock?: boolean} | undefined) - ?.state; - if (!state?.meta?.node || !rootRef.current) { - setPopup(undefined); - return; - } - const mouseEvent = sourceEvent as MouseEvent; - const rect = rootRef.current.getBoundingClientRect(); - const left = Math.min( - Math.max(16, mouseEvent.clientX - rect.left + 12), - rect.width - 456, - ); - const top = Math.min( - Math.max(16, mouseEvent.clientY - rect.top + 12), - rect.height - 240, - ); - setPopup({ - nodeId: state.meta.node.id, - left: Math.max(16, left), - top: Math.max(16, top), - }); - onNodeClick?.(state.meta.node, mouseEvent); - }; - - const selectedNode = popup ? nodes.find((node) => node.id === popup.nodeId) : undefined; - const hoveredNode = nodes.find((node) => node.id === hoveredNodeId); - const defaultContent = selectedNode ? : null; - let popupContent: React.ReactNode = null; - if (selectedNode) { - popupContent = renderNodePopup - ? renderNodePopup({node: selectedNode, defaultContent}) - : defaultContent; - } - - return ( -
{ - setHoveredNodeId(undefined); - setHoverAnchor(undefined); - }} - > - { - if (state === GraphState.ATTACHED) { - if (active) start(); - attachedRef.current = true; - setAttached(true); - } - }} - /> - {active && hoverAnchor && hoveredNode && ( - { - if (!open) setHoverAnchor(undefined); - }} - > - - - )} -
- - - - - - - - - -
- {popup && popupContent !== null && ( -
- {popupContent} -
- )} -
- ); - }, -); - -function changeZoom(graph: ReturnType['graph'], delta: number) { - const camera = graph.cameraService; - const state = camera.getCameraState(); - const nextScale = Math.min(state.scaleMax, Math.max(state.scaleMin, state.scale + delta)); - camera.zoom(state.width / 2, state.height / 2, nextScale); -} - -function State({className, children}: {className?: string; children: React.ReactNode}) { - return ( -
-
{children}
-
- ); -} diff --git a/src/components/QueryGraph/internal/QueryGraphPopup.scss b/src/components/QueryGraph/internal/QueryGraphPopup.scss new file mode 100644 index 0000000..d801a59 --- /dev/null +++ b/src/components/QueryGraph/internal/QueryGraphPopup.scss @@ -0,0 +1,28 @@ +.qp-query-graph { + &__popup-content, + &__jobs, + &__schemas, + &__details { + display: flex; + flex-direction: column; + gap: var(--g-spacing-3); + } + + &__popup-title { + display: flex; + align-items: center; + gap: var(--g-spacing-2); + + > div { + display: flex; + min-width: 0; + flex-direction: column; + } + } + + &__detail-row { + display: flex; + justify-content: space-between; + gap: var(--g-spacing-3); + } +} diff --git a/src/components/QueryGraph/internal/QueryGraphPopup.tsx b/src/components/QueryGraph/internal/QueryGraphPopup.tsx index 4491063..7919cec 100644 --- a/src/components/QueryGraph/internal/QueryGraphPopup.tsx +++ b/src/components/QueryGraph/internal/QueryGraphPopup.tsx @@ -1,3 +1,4 @@ +import './QueryGraphPopup.scss'; import React, {useEffect, useMemo, useState} from 'react'; import {Progress, SegmentedRadioGroup, Table, Text} from '@gravity-ui/uikit'; diff --git a/src/components/QueryGraph/internal/QueryGraphRenderer.scss b/src/components/QueryGraph/internal/QueryGraphRenderer.scss new file mode 100644 index 0000000..7fe13b2 --- /dev/null +++ b/src/components/QueryGraph/internal/QueryGraphRenderer.scss @@ -0,0 +1,50 @@ +@use './queryGraphLayout' as layout; + +.qp-query-graph { + @include layout.root; + &__canvas { + width: 100%; + height: 100%; + min-height: 360px; + } + + &__toolbox { + position: absolute; + z-index: 4; + top: var(--g-spacing-4); + left: var(--g-spacing-4); + display: flex; + width: 28px; + flex-direction: column; + } + + &__toolbox-button { + &_position_top { + --g-button-border-radius: var(--g-border-radius-s) var(--g-border-radius-s) 0 0; + } + + &_position_bottom { + --g-button-border-radius: 0 0 var(--g-border-radius-s) var(--g-border-radius-s); + } + } + + &__hover-anchor { + position: absolute; + width: 1px; + height: 1px; + pointer-events: none; + } + + &__popup { + position: absolute; + z-index: 5; + width: min(440px, calc(100% - var(--g-spacing-8))); + max-height: calc(100% - var(--g-spacing-8)); + overflow: auto; + padding: var(--g-spacing-4); + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-l); + box-shadow: 0 4px 24px var(--g-color-sfx-shadow); + background: var(--g-color-base-float); + } +} diff --git a/src/components/QueryGraph/internal/QueryGraphRenderer.tsx b/src/components/QueryGraph/internal/QueryGraphRenderer.tsx new file mode 100644 index 0000000..39736a7 --- /dev/null +++ b/src/components/QueryGraph/internal/QueryGraphRenderer.tsx @@ -0,0 +1,300 @@ +import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {ECanDrag, GraphState, type TMultipointConnection} from '@gravity-ui/graph'; +import {GraphCanvas, useGraph, useGraphEvent} from '@gravity-ui/graph/react'; +import {MagnifierMinus, MagnifierPlus, SquareDashed} from '@gravity-ui/icons'; +import {Button, Icon, Tooltip, useThemeValue} from '@gravity-ui/uikit'; +import cn from 'bem-cn-lite'; +import type {QueryGraphNode, QueryGraphProps} from '../../../types/queryGraph'; +import i18n from '../i18n'; +import {type QueryGraphBlock, QueryGraphCanvasBlock} from './QueryGraphCanvasBlock'; +import {QueryGraphConnection} from './QueryGraphConnection'; +import {QueryGraphPopup} from './QueryGraphPopup'; +import type {PopupState} from './queryGraphTypes'; +import './QueryGraphRenderer.scss'; +const block = cn('qp-query-graph'); +const FIT_PADDING = 120; +const ZOOM_STEP = 0.08; +const renderEmptyBlock = () => ; + +type RendererProps = Pick< + QueryGraphProps, + 'autoCenter' | 'active' | 'renderNodePopup' | 'onNodeClick' | 'className' +> & { + blocks: QueryGraphBlock[]; + connections: TMultipointConnection[]; + nodes: QueryGraphNode[]; + structureKey: string; + popup?: PopupState; + setPopup: React.Dispatch>; +}; + +export const QueryGraphRenderer = React.forwardRef( + function QueryGraphRendererComponent( + { + blocks, + connections, + nodes, + structureKey, + autoCenter, + active, + popup, + setPopup, + renderNodePopup, + onNodeClick, + className, + }, + forwardedRef, + ) { + const rootRef = useRef(null); + useImperativeHandle(forwardedRef, () => rootRef.current as HTMLDivElement); + const attachedRef = useRef(false); + const centeredRef = useRef(false); + const previousActiveRef = useRef(false); + const previousStructureRef = useRef(structureKey); + const [attached, setAttached] = useState(false); + const [hoveredNodeId, setHoveredNodeId] = useState(); + const [hoverAnchor, setHoverAnchor] = useState<{left: number; top: number}>(); + const themeValue = useThemeValue(); + const {graph, api, setEntities, start, stop} = useGraph({ + name: 'query-graph', + settings: { + connection: QueryGraphConnection, + canDrag: ECanDrag.NONE, + canDragCamera: true, + canZoomCamera: true, + canCreateNewConnections: false, + useBezierConnections: true, + bezierConnectionDirection: 'horizontal', + showConnectionArrows: true, + useBlocksAnchors: false, + blockComponents: {'query-node': QueryGraphCanvasBlock}, + }, + }); + const [scale, setScale] = useState(graph.cameraService.getCameraState().scale); + const {scaleMin, scaleMax} = graph.cameraService.getCameraState(); + useGraphEvent(graph, 'camera-change', ({scale: nextScale}) => { + setScale(nextScale); + setHoverAnchor(undefined); + setHoveredNodeId(undefined); + }); + useGraphEvent(graph, 'mouseenter', ({target, sourceEvent}) => { + const state = (target as {state?: QueryGraphBlock} | undefined)?.state; + if (!state?.meta?.node) return; + setHoveredNodeId(state.meta.node.id); + // GraphLayer wraps synthetic mouseenter events around the original pointer event. + const mouseEvent = + sourceEvent instanceof CustomEvent ? sourceEvent.detail?.sourceEvent : sourceEvent; + if (rootRef.current && mouseEvent instanceof MouseEvent) { + const rect = rootRef.current.getBoundingClientRect(); + setHoverAnchor({ + left: mouseEvent.clientX - rect.left, + top: mouseEvent.clientY - rect.top, + }); + } + }); + useGraphEvent(graph, 'mouseleave', ({target}) => { + const state = (target as {state?: QueryGraphBlock} | undefined)?.state; + if (state?.meta?.node) { + setHoveredNodeId(undefined); + setHoverAnchor(undefined); + } + }); + + useEffect(() => { + if (!rootRef.current) return; + + const background = getComputedStyle(rootRef.current) + .getPropertyValue('--g-color-base-background') + .trim(); + const connection = getComputedStyle(rootRef.current) + .getPropertyValue('--g-color-line-generic') + .trim(); + if (!background || !connection) return; + + const colors = api.getGraphColors(); + api.updateGraphColors({ + ...colors, + canvas: { + ...colors.canvas, + belowLayerBackground: background, + layerBackground: background, + border: background, + dots: background, + }, + connection: { + ...colors.connection, + background: connection, + }, + }); + }, [api, themeValue]); + + const visibleConnections = useMemo( + () => + connections.map((connection) => ({ + ...connection, + selected: Boolean( + hoveredNodeId && + (connection.sourceBlockId === hoveredNodeId || + connection.targetBlockId === hoveredNodeId), + ), + })), + [connections, hoveredNodeId], + ); + useEffect( + () => setEntities({blocks, connections: visibleConnections}), + [blocks, setEntities, visibleConnections], + ); + useEffect(() => { + if (!attachedRef.current) return; + if (active) start(); + else stop(); + }, [active, start, stop]); + useEffect(() => { + const changed = previousStructureRef.current !== structureKey; + const becameActive = active && !previousActiveRef.current; + let frame: number | undefined; + if ( + attached && + active && + (!centeredRef.current || becameActive || (autoCenter && changed)) + ) { + frame = requestAnimationFrame(() => { + api.zoomToViewPort({padding: FIT_PADDING}); + centeredRef.current = true; + }); + } + previousActiveRef.current = Boolean(active); + previousStructureRef.current = structureKey; + setPopup(undefined); + setHoverAnchor(undefined); + setHoveredNodeId(undefined); + return () => { + if (frame !== undefined) cancelAnimationFrame(frame); + }; + }, [active, api, attached, autoCenter, setPopup, structureKey]); + + const handleClick: NonNullable['click']> = ({ + target, + sourceEvent, + }) => { + setHoverAnchor(undefined); + const state = (target as {state?: QueryGraphBlock; isBlock?: boolean} | undefined) + ?.state; + if (!state?.meta?.node || !rootRef.current) { + setPopup(undefined); + return; + } + const mouseEvent = sourceEvent as MouseEvent; + const rect = rootRef.current.getBoundingClientRect(); + const left = Math.min( + Math.max(16, mouseEvent.clientX - rect.left + 12), + rect.width - 456, + ); + const top = Math.min( + Math.max(16, mouseEvent.clientY - rect.top + 12), + rect.height - 240, + ); + setPopup({ + nodeId: state.meta.node.id, + left: Math.max(16, left), + top: Math.max(16, top), + }); + onNodeClick?.(state.meta.node, mouseEvent); + }; + + const selectedNode = popup ? nodes.find((node) => node.id === popup.nodeId) : undefined; + const hoveredNode = nodes.find((node) => node.id === hoveredNodeId); + const defaultContent = selectedNode ? : null; + let popupContent: React.ReactNode = null; + if (selectedNode) { + popupContent = renderNodePopup + ? renderNodePopup({node: selectedNode, defaultContent}) + : defaultContent; + } + + return ( +
{ + setHoveredNodeId(undefined); + setHoverAnchor(undefined); + }} + > + { + if (state === GraphState.ATTACHED) { + if (active) start(); + attachedRef.current = true; + setAttached(true); + } + }} + /> + {active && hoverAnchor && hoveredNode && ( + { + if (!open) setHoverAnchor(undefined); + }} + > + + + )} +
+ + + + + + + + + +
+ {popup && popupContent !== null && ( +
+ {popupContent} +
+ )} +
+ ); + }, +); + +function changeZoom(graph: ReturnType['graph'], delta: number) { + const camera = graph.cameraService; + const state = camera.getCameraState(); + const nextScale = Math.min(state.scaleMax, Math.max(state.scaleMin, state.scale + delta)); + camera.zoom(state.width / 2, state.height / 2, nextScale); +} diff --git a/src/components/QueryGraph/internal/QueryGraphState.scss b/src/components/QueryGraph/internal/QueryGraphState.scss new file mode 100644 index 0000000..8f5d44f --- /dev/null +++ b/src/components/QueryGraph/internal/QueryGraphState.scss @@ -0,0 +1,14 @@ +@use './queryGraphLayout' as layout; + +.qp-query-graph { + @include layout.root; + &__state { + height: 100%; + min-height: 360px; + display: flex; + align-items: center; + justify-content: center; + padding: var(--g-spacing-6); + text-align: center; + } +} diff --git a/src/components/QueryGraph/internal/QueryGraphState.tsx b/src/components/QueryGraph/internal/QueryGraphState.tsx new file mode 100644 index 0000000..9d1151b --- /dev/null +++ b/src/components/QueryGraph/internal/QueryGraphState.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import cn from 'bem-cn-lite'; +import './QueryGraphState.scss'; +const block = cn('qp-query-graph'); + +export function QueryGraphState({ + className, + children, +}: { + className?: string; + children: React.ReactNode; +}) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/components/QueryGraph/internal/QueryGraphWithLayout.tsx b/src/components/QueryGraph/internal/QueryGraphWithLayout.tsx new file mode 100644 index 0000000..e9a69c7 --- /dev/null +++ b/src/components/QueryGraph/internal/QueryGraphWithLayout.tsx @@ -0,0 +1,133 @@ +import React, {useMemo, useRef, useState} from 'react'; +import type {TMultipointConnection} from '@gravity-ui/graph'; +import {Loader, Text} from '@gravity-ui/uikit'; +import type {QueryGraphProps} from '../../../types/queryGraph'; +import {createQueryGraphConnections, getQueryGraphNodeSize} from '../helpers/layout'; +import type {QueryGraphBlock} from './QueryGraphCanvasBlock'; +import {getQueryGraphNodeIconSvgs} from './queryGraphIcons'; +import {useQueryGraphLayout} from './useQueryGraphLayout'; +import {QueryGraphRenderer} from './QueryGraphRenderer'; +import type {PopupState} from './queryGraphTypes'; +import {QueryGraphState as State} from './QueryGraphState'; + +type QueryGraphWithLayoutProps = Pick< + QueryGraphProps, + | 'nodes' + | 'edges' + | 'autoCenter' + | 'active' + | 'renderNodePopup' + | 'onNodeClick' + | 'onError' + | 'className' +> & {structureKey: string}; + +export function QueryGraphWithLayout({ + nodes, + edges, + structureKey, + autoCenter, + active, + renderNodePopup, + onNodeClick, + onError, + className, +}: QueryGraphWithLayoutProps) { + const [popup, setPopup] = useState(); + const rootRef = useRef(null); + const nodesRef = useRef(nodes); + const edgesRef = useRef(edges); + nodesRef.current = nodes; + edgesRef.current = edges; + + const layoutBlocks = useMemo( + () => + nodesRef.current.map((node) => ({ + id: node.id, + ...getQueryGraphNodeSize(node), + })), + // Node status and presentation do not affect layout. + // eslint-disable-next-line react-hooks/exhaustive-deps + [structureKey], + ); + const layoutConnections = useMemo( + () => + edgesRef.current.map((edge) => ({ + id: edge.id, + sourceBlockId: edge.source, + targetBlockId: edge.target, + })), + // Only graph structure affects layout. + // eslint-disable-next-line react-hooks/exhaustive-deps + [structureKey], + ); + const {result, resolvedStructureKey, isInitialLoading, error} = useQueryGraphLayout({ + structureKey, + blocks: layoutBlocks, + connections: layoutConnections, + onError, + }); + + const graphBlocks = useMemo( + () => + nodes.flatMap((node) => { + const position = result?.blocks[node.id]; + if (!position) return []; + const {iconSvg, fallbackIconSvg, iconColorToken, fallbackIconColorToken} = + getQueryGraphNodeIconSvgs(node); + return [ + { + id: node.id, + is: 'query-node', + name: node.name, + selected: false, + anchors: [], + meta: { + node, + iconSvg, + fallbackIconSvg, + iconColorToken, + fallbackIconColorToken, + }, + ...getQueryGraphNodeSize(node), + ...position, + }, + ]; + }), + [nodes, result], + ); + const graphConnections = useMemo( + () => createQueryGraphConnections(edges, result?.edges), + [edges, result], + ); + + if (error) + return ( + + {error.message} + + ); + if (isInitialLoading || !result || !resolvedStructureKey) + return ( + + + + ); + + return ( + + ); +} diff --git a/src/components/QueryGraph/internal/_queryGraphLayout.scss b/src/components/QueryGraph/internal/_queryGraphLayout.scss new file mode 100644 index 0000000..6dc6043 --- /dev/null +++ b/src/components/QueryGraph/internal/_queryGraphLayout.scss @@ -0,0 +1,7 @@ +@mixin root { + position: relative; + min-height: 360px; + height: 100%; + overflow: hidden; + background: var(--g-color-base-background); +} diff --git a/src/components/QueryGraph/internal/queryGraphTypes.ts b/src/components/QueryGraph/internal/queryGraphTypes.ts new file mode 100644 index 0000000..c04abb4 --- /dev/null +++ b/src/components/QueryGraph/internal/queryGraphTypes.ts @@ -0,0 +1 @@ +export type PopupState = {nodeId: string; left: number; top: number}; diff --git a/src/components/QueryGraph/story/demoData.ts b/src/components/QueryGraph/story/demoData.ts new file mode 100644 index 0000000..f52590c --- /dev/null +++ b/src/components/QueryGraph/story/demoData.ts @@ -0,0 +1,58 @@ +import type {QueryGraphEdge, QueryGraphNode} from '../../../types/queryGraph'; + +export const demoNodes: QueryGraphNode[] = [ + {id: 'source', kind: 'input', name: 'orders', label: 'warehouse.orders'}, + { + id: 'read', + kind: 'operation', + name: 'Read', + operationType: 'read', + status: 'completed', + progress: {total: 12, completed: 12}, + }, + { + id: 'map', + kind: 'operation', + name: 'Map', + operationType: 'map', + status: 'running', + progress: {total: 100, completed: 68, running: 8, pending: 24}, + popup: { + stages: [ + {name: 'Preparing', duration: 1300}, + {name: 'Executing', duration: 8400}, + ], + details: [ + {name: 'Columns', value: ['id', 'price', 'created_at']}, + {name: 'Settings', children: [{name: 'Workers', value: 8}]}, + ], + jobs: {total: 100, completed: 68, running: 8, pending: 24}, + inputs: [ + { + name: 'orders', + columns: [ + {name: 'id', type: 'Uint64'}, + {name: 'price', type: 'Double'}, + ], + }, + ], + outputs: [{name: 'mapped', columns: [{name: 'total', type: 'Double'}]}], + }, + }, + { + id: 'reduce', + kind: 'operation', + name: 'Reduce', + operationType: 'reduce', + status: 'waiting', + progress: {total: 20}, + }, + {id: 'sink', kind: 'output', name: 'result', label: 'warehouse.result'}, +]; + +export const demoEdges: QueryGraphEdge[] = [ + {id: 'source-read', source: 'source', target: 'read'}, + {id: 'read-map', source: 'read', target: 'map'}, + {id: 'map-reduce', source: 'map', target: 'reduce'}, + {id: 'reduce-sink', source: 'reduce', target: 'sink'}, +]; diff --git a/src/components/QueryStatusIcon/QueryStatusIcon.tsx b/src/components/QueryStatusIcon/QueryStatusIcon.tsx index efee987..817dd6e 100644 --- a/src/components/QueryStatusIcon/QueryStatusIcon.tsx +++ b/src/components/QueryStatusIcon/QueryStatusIcon.tsx @@ -1,19 +1,16 @@ -import React, {FC} from 'react'; -import {Icon, IconData, Spin} from '@gravity-ui/uikit'; import CircleCheckIcon from '@gravity-ui/icons/svgs/circle-check.svg'; import CircleExclamationIcon from '@gravity-ui/icons/svgs/circle-exclamation.svg'; -import FileIcon from '@gravity-ui/icons/svgs/file.svg'; -import CircleStopIcon from '@gravity-ui/icons/svgs/circle-stop.svg'; import CirclePlayIcon from '@gravity-ui/icons/svgs/circle-play.svg'; -import {QueryStatus} from '../../types/history'; +import CircleStopIcon from '@gravity-ui/icons/svgs/circle-stop.svg'; +import FileIcon from '@gravity-ui/icons/svgs/file.svg'; +import {Icon, IconData, Spin} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import './QueryStatusIcon.scss'; +import React, {FC} from 'react'; import {ProgressQueryStatuses} from '../../constants/query'; - -type Props = { - status: QueryStatus; - className?: string; -}; +import {QueryStatus} from '../../types/history'; +import type {QueryStatusIconProps as Props} from '../../types/queryStatusIconProps'; +import './QueryStatusIcon.scss'; +export type {QueryStatusIconProps as Props} from '../../types/queryStatusIconProps'; const STATUS_ICONS: Record = { completed: CircleCheckIcon, diff --git a/src/components/QueryStatusIcon/index.ts b/src/components/QueryStatusIcon/index.ts index b492fdd..84dcf9d 100644 --- a/src/components/QueryStatusIcon/index.ts +++ b/src/components/QueryStatusIcon/index.ts @@ -1 +1,3 @@ export {QueryStatusIcon} from './QueryStatusIcon'; + +export type {QueryStatusIconProps} from '../../types/queryStatusIconProps'; diff --git a/src/components/RowActionsMenu/RowActionsMenu.tsx b/src/components/RowActionsMenu/RowActionsMenu.tsx index 3e12f8e..ce210a6 100644 --- a/src/components/RowActionsMenu/RowActionsMenu.tsx +++ b/src/components/RowActionsMenu/RowActionsMenu.tsx @@ -1,12 +1,9 @@ -import React, {MouseEvent, useMemo} from 'react'; -import {Button, DropdownMenu, DropdownMenuItem, Icon} from '@gravity-ui/uikit'; import EllipsisIcon from '@gravity-ui/icons/svgs/ellipsis.svg'; -import {QueryListRow, QueryListRowAction} from '../../types/queryList'; - -export type RowActionsMenuProps = { - item: T; - actions?: QueryListRowAction[]; -}; +import {Button, DropdownMenu, DropdownMenuItem, Icon} from '@gravity-ui/uikit'; +import React, {MouseEvent, useMemo} from 'react'; +import {QueryListRow} from '../../types/queryList'; +import type {RowActionsMenuProps} from '../../types/rowActionsMenuProps'; +export type {RowActionsMenuProps} from '../../types/rowActionsMenuProps'; export const RowActionsMenu = ({item, actions}: RowActionsMenuProps) => { const items = useMemo(() => { diff --git a/src/components/RowLink/RowLink.tsx b/src/components/RowLink/RowLink.tsx index 06c757b..b4b61f9 100644 --- a/src/components/RowLink/RowLink.tsx +++ b/src/components/RowLink/RowLink.tsx @@ -1,16 +1,11 @@ -import React, {FC} from 'react'; import cn from 'bem-cn-lite'; -import {QueryListLinkRenderer} from '../../types/queryList'; +import React, {FC} from 'react'; +import type {RowLinkProps} from '../../types/rowLinkProps'; import './RowLink.scss'; +export type {RowLinkProps} from '../../types/rowLinkProps'; const block = cn('qp-row-link'); -export type RowLinkProps = Omit, 'href'> & { - href?: string; - disabled?: boolean; - renderLink?: QueryListLinkRenderer; -}; - export const RowLink: FC = ({ href, disabled, diff --git a/src/components/SearchRowLayout/SearchRowLayout.scss b/src/components/SearchRowLayout/SearchRowLayout.scss index 15b2a4e..751bd6d 100644 --- a/src/components/SearchRowLayout/SearchRowLayout.scss +++ b/src/components/SearchRowLayout/SearchRowLayout.scss @@ -4,15 +4,26 @@ $search-row-header-height: 20px; $search-row-gap: 8px; .qp-search-row-layout { - height: $search-row-height; + height: $search-row-height; - &__content { - height: 100%; - } + &__content { + height: 100%; + } - &__monaco { - height: $search-row-height - 2 * $search-row-vertical-padding - $search-row-header-height - - $search-row-gap; - border: 1px solid var(--g-color-line-generic-accent); - } + // Keep preview dimensions stable when the lazy editor stylesheet arrives. + & &__monaco { + box-sizing: border-box; + height: $search-row-height - 2 * $search-row-vertical-padding - $search-row-header-height - + $search-row-gap; + border: 1px solid var(--g-color-line-generic-accent); + + &_loading { + margin: 0; + overflow: hidden; + font-family: var(--g-font-family-monospace); + font-size: 12px; + line-height: 18px; + white-space: pre; + } + } } diff --git a/src/components/SearchRowLayout/SearchRowLayout.tsx b/src/components/SearchRowLayout/SearchRowLayout.tsx index 3bee4b3..1a1f9be 100644 --- a/src/components/SearchRowLayout/SearchRowLayout.tsx +++ b/src/components/SearchRowLayout/SearchRowLayout.tsx @@ -1,24 +1,18 @@ -import React, {FC, ReactNode} from 'react'; import {Flex} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import {MonacoEditor} from '../MonacoEditor'; +import React, {FC, Suspense, lazy} from 'react'; +import type {SearchRowLayoutProps} from '../../types/searchRowLayoutProps'; import {RowLink} from '../RowLink'; -import {QueryListLinkRenderer} from '../../types/queryList'; import {fitQueryToVisibleLines} from './helpers/fitQueryToVisibleLines'; import {MONACO_CONFIG} from './helpers/monacoConfig'; import './SearchRowLayout.scss'; +export type {SearchRowLayoutProps} from '../../types/searchRowLayoutProps'; -const block = cn('qp-search-row-layout'); +const MonacoEditor = lazy(() => + import('../MonacoEditor').then((module) => ({default: module.MonacoEditor})), +); -export type SearchRowLayoutProps = { - header: ReactNode; - query?: string; - language: string; - href?: string; - disabled?: boolean; - renderLink?: QueryListLinkRenderer; - className?: string; -}; +const block = cn('qp-search-row-layout'); export const SearchRowLayout: FC = ({ header, @@ -29,6 +23,8 @@ export const SearchRowLayout: FC = ({ renderLink, className, }) => { + const visibleQuery = fitQueryToVisibleLines(query); + return ( = ({ {header} - + {visibleQuery} + } + > + +
); diff --git a/src/components/SearchWithButtons/SearchWithButtons.tsx b/src/components/SearchWithButtons/SearchWithButtons.tsx index b720c3b..5ac7090 100644 --- a/src/components/SearchWithButtons/SearchWithButtons.tsx +++ b/src/components/SearchWithButtons/SearchWithButtons.tsx @@ -1,18 +1,9 @@ -import React, {FC} from 'react'; -import {Flex, type FlexProps, TextInput} from '@gravity-ui/uikit'; +import {Flex, TextInput} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {FC} from 'react'; +import type {SearchWithButtonsProps} from '../../types/searchWithButtonsProps'; import './SearchWithButtons.scss'; - -export type SearchWithButtonsProps = { - value?: string; - placeholder?: string; - hasClear?: boolean; - innerButtons?: React.ReactNode[]; - endButtons?: React.ReactNode[]; - onUpdate?: (value: string) => void; - className?: string; - gap?: FlexProps['gap']; -}; +export type {SearchWithButtonsProps} from '../../types/searchWithButtonsProps'; const block = cn('qp-search-with-buttons'); diff --git a/src/components/SimpleForm/CheckboxGroupField.tsx b/src/components/SimpleForm/CheckboxGroupField.tsx index 6224271..39bc076 100644 --- a/src/components/SimpleForm/CheckboxGroupField.tsx +++ b/src/components/SimpleForm/CheckboxGroupField.tsx @@ -1,15 +1,7 @@ +import type {CheckboxGroupFieldProps} from '../../types/simpleFormProps'; import React, {FC} from 'react'; import {Checkbox, Flex, Text} from '@gravity-ui/uikit'; -type CheckboxGroupItem = {id: string; title?: string}; - -type CheckboxGroupFieldProps = { - value: string[]; - items: CheckboxGroupItem[]; - title?: string; - onChange: (value: string[]) => void; -}; - export const CheckboxGroupField: FC = ({ value = [], items, diff --git a/src/components/SimpleForm/RangeDatePickerField.tsx b/src/components/SimpleForm/RangeDatePickerField.tsx index 7de91c9..b9fa476 100644 --- a/src/components/SimpleForm/RangeDatePickerField.tsx +++ b/src/components/SimpleForm/RangeDatePickerField.tsx @@ -1,15 +1,7 @@ +import type {DateValue, RangeDatePickerFieldProps} from '../../types/simpleFormProps'; import React, {FC} from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; -import {DatePicker, RangeValue} from '@gravity-ui/date-components'; -import {DateTime} from '@gravity-ui/date-utils'; - -type DateValue = DateTime | null; - -export type RangeDatePickerFieldProps = { - value: RangeValue; - title?: string; - onChange: (value: RangeValue) => void; -}; +import {DatePicker} from '@gravity-ui/date-components'; const DATE_FORMAT = 'YYYY-MM-DD'; diff --git a/src/components/SimpleForm/SelectField.tsx b/src/components/SimpleForm/SelectField.tsx index 418b077..c6dedc7 100644 --- a/src/components/SimpleForm/SelectField.tsx +++ b/src/components/SimpleForm/SelectField.tsx @@ -1,12 +1,6 @@ +import type {SelectFieldProps} from '../../types/simpleFormProps'; import React, {FC} from 'react'; -import {Flex, Select, SelectProps, Text} from '@gravity-ui/uikit'; - -export type SelectFieldProps = { - value: SelectProps['value']; - options: SelectProps['options']; - title?: string; - onChange: (value: string[]) => void; -}; +import {Flex, Select, Text} from '@gravity-ui/uikit'; export const SelectField: FC = ({value, options, title, onChange}) => { return ( diff --git a/src/components/SimpleForm/SimpleForm.tsx b/src/components/SimpleForm/SimpleForm.tsx index 067a35b..332c65e 100644 --- a/src/components/SimpleForm/SimpleForm.tsx +++ b/src/components/SimpleForm/SimpleForm.tsx @@ -1,24 +1,19 @@ +import {Flex} from '@gravity-ui/uikit'; import React, {useMemo, useState} from 'react'; -import {RegisteredFormFields} from '../../types/forms'; +import type {FormValues, SimpleFormProps} from '../../types/simpleFormProps'; +import {CheckboxGroupField} from './CheckboxGroupField'; import {getControl, getInitialValues, registerFormControl} from './helpers'; -import {SwitchField} from './SwitchField'; import {RangeDatePickerField} from './RangeDatePickerField'; -import {Flex} from '@gravity-ui/uikit'; -import {CheckboxGroupField} from './CheckboxGroupField'; import {SelectField} from './SelectField'; - -export type FormField = RegisteredFormFields; - -type FormValues = Record; - -type Props = { - fields: FormField[]; - values?: FormValues; - initialValues?: FormValues; - onValuesChange?: (values: FormValues) => void; -}; - -export const SimpleForm: React.FC = ({fields, values, initialValues, onValuesChange}) => { +import {SwitchField} from './SwitchField'; +export type {FormField} from '../../types/simpleFormProps'; + +export const SimpleForm: React.FC = ({ + fields, + values, + initialValues, + onValuesChange, +}) => { const isControlled = values !== undefined; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/SimpleForm/SwitchField.tsx b/src/components/SimpleForm/SwitchField.tsx index 50ebee9..7be5c2c 100644 --- a/src/components/SimpleForm/SwitchField.tsx +++ b/src/components/SimpleForm/SwitchField.tsx @@ -1,12 +1,7 @@ +import type {SwitchFieldProps} from '../../types/simpleFormProps'; import React, {FC} from 'react'; import {Flex, Switch, Text} from '@gravity-ui/uikit'; -export type SwitchFieldProps = { - value: boolean; - title?: string; - onChange: (value: boolean) => void; -}; - export const SwitchField: FC = ({title, value, onChange}) => { return ( diff --git a/src/components/SimpleForm/helpers/getInitialValues.ts b/src/components/SimpleForm/helpers/getInitialValues.ts index 8c49046..66d575e 100644 --- a/src/components/SimpleForm/helpers/getInitialValues.ts +++ b/src/components/SimpleForm/helpers/getInitialValues.ts @@ -1,6 +1,4 @@ -import {FormField} from '../SimpleForm'; - -type FormValues = Record; +import type {FormField, FormValues} from '../../../types/simpleFormProps'; export function getInitialValues(fields: FormField[], initialValues?: FormValues): FormValues { return { diff --git a/src/components/SimpleForm/index.ts b/src/components/SimpleForm/index.ts index 008cd68..eaf0897 100644 --- a/src/components/SimpleForm/index.ts +++ b/src/components/SimpleForm/index.ts @@ -1,7 +1,15 @@ export {SimpleForm} from './SimpleForm'; -export type {FormField} from './SimpleForm'; +export type { + FormField, + FormValues, + SimpleFormProps, + SelectFieldProps, + CheckboxGroupFieldProps, + CheckboxGroupItem, + DateValue, +} from '../../types/simpleFormProps'; export {SwitchField} from './SwitchField'; -export type {SwitchFieldProps} from './SwitchField'; +export type {SwitchFieldProps} from '../../types/simpleFormProps'; export {RangeDatePickerField} from './RangeDatePickerField'; -export type {RangeDatePickerFieldProps} from './RangeDatePickerField'; +export type {RangeDatePickerFieldProps} from '../../types/simpleFormProps'; export {registerFormControl} from './helpers'; diff --git a/src/components/SkeletonRows/SkeletonRows.tsx b/src/components/SkeletonRows/SkeletonRows.tsx index 15f8445..bf143c5 100644 --- a/src/components/SkeletonRows/SkeletonRows.tsx +++ b/src/components/SkeletonRows/SkeletonRows.tsx @@ -1,11 +1,7 @@ -import React, {FC} from 'react'; import {Flex, Skeleton} from '@gravity-ui/uikit'; - -export type SkeletonRowsProps = { - count?: number; - className?: string; - rowClassName?: string; -}; +import React, {FC} from 'react'; +import type {SkeletonRowsProps} from '../../types/skeletonRowsProps'; +export type {SkeletonRowsProps} from '../../types/skeletonRowsProps'; const DEFAULT_ROWS_COUNT = 4; diff --git a/src/helpers/chartLabels/i18n/dicts.ts b/src/helpers/chartLabels/i18n/dicts.ts new file mode 100644 index 0000000..99e66a4 --- /dev/null +++ b/src/helpers/chartLabels/i18n/dicts.ts @@ -0,0 +1,3 @@ +import en from './en.json'; +import ru from './ru.json'; +export default {en, ru}; diff --git a/src/helpers/chartLabels/i18n/en.json b/src/helpers/chartLabels/i18n/en.json new file mode 100644 index 0000000..cede3ed --- /dev/null +++ b/src/helpers/chartLabels/i18n/en.json @@ -0,0 +1,15 @@ +{ + "value_area": "Area", + "value_bar-x": "Vertical bars", + "value_bar-y": "Horizontal bars", + "value_line": "Line", + "value_pie": "Pie", + "value_scatter": "Scatter", + "value_waterfall": "Waterfall", + "value_funnel": "Funnel", + "value_heatmap": "Heatmap", + "value_radar": "Radar", + "value_sankey": "Sankey", + "value_treemap": "Treemap", + "value_x-range": "Range" +} diff --git a/src/helpers/chartLabels/i18n/index.ts b/src/helpers/chartLabels/i18n/index.ts new file mode 100644 index 0000000..7c0a532 --- /dev/null +++ b/src/helpers/chartLabels/i18n/index.ts @@ -0,0 +1,4 @@ +import {addI18Keysets} from '../../../i18n'; +import dicts from './dicts'; +// Chart type labels shared by standalone controls and dashboards. +export default addI18Keysets('qp:chart-types', dicts); diff --git a/src/helpers/chartLabels/i18n/ru.json b/src/helpers/chartLabels/i18n/ru.json new file mode 100644 index 0000000..2d8fb2f --- /dev/null +++ b/src/helpers/chartLabels/i18n/ru.json @@ -0,0 +1,15 @@ +{ + "value_area": "С областями", + "value_bar-x": "Вертикальные столбцы", + "value_bar-y": "Горизонтальные столбцы", + "value_line": "Линейный", + "value_pie": "Круговой", + "value_scatter": "Точечный", + "value_waterfall": "Водопад", + "value_funnel": "Воронка", + "value_heatmap": "Тепловая карта", + "value_radar": "Радар", + "value_sankey": "Санкей", + "value_treemap": "Древовидная карта", + "value_x-range": "Диапазоны" +} diff --git a/src/helpers/chartLabels/index.ts b/src/helpers/chartLabels/index.ts new file mode 100644 index 0000000..2ea8305 --- /dev/null +++ b/src/helpers/chartLabels/index.ts @@ -0,0 +1,6 @@ +import type {ChartSeries} from '@gravity-ui/charts'; +import i18n from './i18n'; + +export function getChartTypeLabel(type: ChartSeries['type']): string { + return i18n(`value_${type}`); +} diff --git a/src/index.ts b/src/index.ts index 0edd24a..8e94f8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ export * from './types/queryEditor'; export * from './types/queryShare'; export * from './types/querySettings'; export * from './types/queryParameters'; -export * from './types/ytsaurusQueryEngines'; +export * from './types/builtinQueryEngines'; export type {MonacoEditorProps, MonacoEditorIntegrationProps} from './types/monacoEditor'; export * from './types/editorProviders'; export * from './types/queryEditorPreset'; @@ -33,6 +33,75 @@ export type {QueriesHistoryProps} from './types/queriesHistory'; export type {QueriesNavigationProps} from './types/queriesNavigation'; export type {SavedQueriesProps} from './types/savedQueriesProps'; export type {TutorialsHistoryProps} from './types/tutorialsHistory'; -export type {AttachmentsProps, AttachItem} from './types/attachments'; +export type {AttachmentsProps, AttachItem, Attachment} from './types/attachments'; export * from './types/monacoDiffEditor'; export * from './types/queryComparison'; + +export type {AttachmentListProps} from './types/attachmentListProps'; +export type {AttachmentListPlaceholderProps} from './types/attachmentListPlaceholderProps'; +export type {BreadcrumbsProps} from './types/breadcrumbsProps'; +export type {ChartAction, ChartProps} from './types/chartProps'; +export type {ClusterRowProps} from './types/clusterRowProps'; +export type {DataTableProps} from './types/dataTableProps'; +export type {EditableRowTitleProps} from './types/editableRowTitleProps'; +export type {EmptyContentVariant, EmptyContentProps} from './types/emptyContentProps'; +export type {FieldsSearchToolbarProps} from './types/fieldsSearchToolbarProps'; +export type {FieldsSelectorOption, FieldsSelectorProps} from './types/fieldsSelectorProps'; +export type {LazyListProps} from './types/lazyListProps'; +export type {ListSpinnerProps} from './types/listSpinnerProps'; +export type {NavigationActionButtonsProps} from './types/navigationActionButtonsProps'; +export type {NavigationItemRowProps} from './types/navigationItemRowProps'; +export type {PathEditorProps} from './types/pathEditorProps'; +export type {RowActionsMenuProps} from './types/rowActionsMenuProps'; +export type {RowLinkProps} from './types/rowLinkProps'; +export type {SearchRowLayoutProps} from './types/searchRowLayoutProps'; +export type {SearchWithButtonsProps} from './types/searchWithButtonsProps'; +export type { + FormField as SimpleFormField, + FormValues, + SimpleFormProps, + SwitchFieldProps, + RangeDatePickerFieldProps, + SelectFieldProps, + CheckboxGroupFieldProps, + CheckboxGroupItem, + DateValue, +} from './types/simpleFormProps'; +export type {SkeletonRowsProps} from './types/skeletonRowsProps'; +export type {ClustersListProps} from './types/clustersListProps'; +export type {HistoryComparisonActionsProps} from './types/historyComparisonActionsProps'; +export type {HistoryLayoutProps} from './types/historyLayoutProps'; +export type {HistoryRowProps} from './types/historyRowProps'; +export type {HistorySearchRowProps} from './types/historySearchRowProps'; +export type {NavigationHeaderProps} from './types/navigationHeaderProps'; +export type {NavigationItemsListProps} from './types/navigationItemsListProps'; +export type {NavigationMetaViewConfig, NavigationMetaProps} from './types/navigationMetaProps'; +export type { + NavigationSchemaViewConfig, + NavigationSchemaProps, +} from './types/navigationSchemaProps'; +export type {NavigationViewViewConfig, NavigationViewProps} from './types/navigationViewProps'; +export type {QueriesListProps} from './types/queriesListProps'; +export type {RowsListProps} from './types/rowsListProps'; +export type {SavedQueryRowProps} from './types/savedQueryRowProps'; +export type {SavedQuerySearchRowProps} from './types/savedQuerySearchRowProps'; +export type {TutorialRowProps} from './types/tutorialRowProps'; +export type {TutorialSearchRowProps} from './types/tutorialSearchRowProps'; + +export type {DashboardItemAppearance, DashboardRenderItem, DashboardProps} from './types/dashboard'; +export type { + AddChartButtonOption, + AddChartButtonProps, + DefaultAddChartButtonValue, +} from './types/addChartButton'; + +export type {HistoryGroupHeaderProps} from './types/historyGroupHeaderProps'; +export type {HistoryPrivateIconProps} from './types/historyPrivateIconProps'; +export type {QueryDurationProps} from './types/queryDurationProps'; +export type {QueryStatusIconProps} from './types/queryStatusIconProps'; +export type {HistoryHeaderProps} from './types/historyHeaderProps'; + +export type {HistoryFilterProps} from './types/historyFilter'; +export type {NavigationDetailProps} from './types/navigationDetail'; +export * from './types/navigationDetailResolvers'; +export * from './types/attachmentsEditor'; diff --git a/src/modules/Attachments/Attachments.tsx b/src/modules/Attachments/Attachments.tsx index 129565e..861f230 100644 --- a/src/modules/Attachments/Attachments.tsx +++ b/src/modules/Attachments/Attachments.tsx @@ -11,7 +11,7 @@ import { Text, } from '@gravity-ui/uikit'; import {AttachmentList, EditAttachmentItem} from '../../components/AttachmentList'; -import type {EditLinkValues} from '../../components/AttachmentList'; +import type {EditLinkValues} from '../../types/attachmentsEditor'; import {AttachmentListPlaceholder} from '../../components/AttachmentListPlaceholder'; import type {AttachItem, AttachmentsProps} from '../../types/attachments'; import {createUuid} from '../../helpers/createUuid'; @@ -78,83 +78,32 @@ export const Attachments = ({ setWasEditedIds([...wasEdited, attachId]); }; - const handleAddEmptyFile = () => { + const addEmptyAttachment = (values: Omit) => { const id = createUuid(); - - const patchAttachList = [ - ...attachList.filter((a) => !a.isNew), - {id, name: '', isNew: true}, - ]; - + const patchAttachList = [...attachList.filter((a) => !a.isNew), {id, ...values}]; setInnerAttachList(patchAttachList); setWasAddedIds([...wasAddedIds, id]); setEditingIds([...editingIds, id]); - - onChange?.({ - attachments: patchAttachList, - deletedAttachments: deletedAttachList, - }); - }; - - const handleAddEmptyLink = () => { - const id = createUuid(); - - const patchAttachList = [ - ...attachList.filter((a) => !a.isNew), - {id, name: '', token: '', link: '', isNew: true}, - ]; - - setInnerAttachList(patchAttachList); - setWasAddedIds([...wasAddedIds, id]); - setEditingIds([...editingIds, id]); - - onChange?.({ - attachments: patchAttachList, - deletedAttachments: deletedAttachList, - }); + onChange?.({attachments: patchAttachList, deletedAttachments: deletedAttachList}); }; - const handleAcceptFile = (fileId: string, fileName: string) => { - const patchedFile = attachList.find((a) => a.id === fileId); - if (!patchedFile) return; - - const patchEditingIds = editingIds.filter((id) => id !== fileId); - const patchAttachList = attachList.map((attach) => { - if (attach.id === fileId) return {id: fileId, name: fileName}; - return attach; - }); + const handleAddEmptyFile = () => addEmptyAttachment({name: '', isNew: true}); + const handleAddEmptyLink = () => + addEmptyAttachment({name: '', token: '', link: '', isNew: true}); - setEditingIds(patchEditingIds); + const acceptAttachment = (nextAttachment: AttachItem) => { + if (!attachList.some((attach) => attach.id === nextAttachment.id)) return; + const patchAttachList = attachList.map((attach) => + attach.id === nextAttachment.id ? nextAttachment : attach, + ); + setEditingIds(editingIds.filter((id) => id !== nextAttachment.id)); setInnerAttachList(patchAttachList); - - onChange?.({ - attachments: patchAttachList, - deletedAttachments: deletedAttachList, - }); - - runAfterAttachAdded(fileId, {id: fileId, name: fileName}); + onChange?.({attachments: patchAttachList, deletedAttachments: deletedAttachList}); + runAfterAttachAdded(nextAttachment.id, nextAttachment); }; - const handleAcceptLink = (linkId: string, link: EditLinkValues) => { - const patchedLink = attachList.find((a) => a.id === linkId); - if (!patchedLink) return; - - const patchEditingIds = editingIds.filter((id) => id !== linkId); - const patchAttachList = attachList.map((attach) => { - if (attach.id === linkId) return {id: linkId, ...link}; - return attach; - }); - - setEditingIds(patchEditingIds); - setInnerAttachList(patchAttachList); - - onChange?.({ - attachments: patchAttachList, - deletedAttachments: deletedAttachList, - }); - - runAfterAttachAdded(linkId, {id: linkId, ...link}); - }; + const handleAcceptFile = (id: string, name: string) => acceptAttachment({id, name}); + const handleAcceptLink = (id: string, link: EditLinkValues) => acceptAttachment({id, ...link}); const handleCancelEdit = (fileId: string) => { const patchEditingIds = editingIds.filter((id) => id !== fileId); diff --git a/src/modules/Attachments/index.ts b/src/modules/Attachments/index.ts index 4388650..855bb74 100644 --- a/src/modules/Attachments/index.ts +++ b/src/modules/Attachments/index.ts @@ -1,2 +1,2 @@ export {Attachments} from './Attachments'; -export type {AttachmentsProps, AttachItem} from '../../types/attachments'; +export type {AttachmentsProps, AttachItem, Attachment} from '../../types/attachments'; diff --git a/src/modules/ChartEditor/types.ts b/src/modules/ChartEditor/types.ts index 2143ded..d8b4b99 100644 --- a/src/modules/ChartEditor/types.ts +++ b/src/modules/ChartEditor/types.ts @@ -1,16 +1 @@ -import type {ChartEditorFormProps} from '../../components/ChartEditorForm'; -import type {ChartData, ChartSeries} from '../../types/chartEditor'; -export type {ChartData, ChartSeries} from '../../types/chartEditor'; - -export type ChartEditorProps = Pick & { - className?: string; - emptyDataLabel?: string; - chartSeriesMap?: { - [chartId in string]: ChartSeries; - }; - formProps?: Pick; - formValues?: ChartEditorFormProps['formValues']; - onChange?: (formValues: ChartEditorFormProps['formValues']) => void; - onSubmit?: (chartData: ChartData) => void; - onCancel?: () => void; -}; +export type {ChartData, ChartSeries, ChartEditorProps} from '../../types/chartEditor'; diff --git a/src/modules/ChartFieldsEditor/ChartFieldsEditor.stories.tsx b/src/modules/ChartFieldsEditor/ChartFieldsEditor.stories.tsx new file mode 100644 index 0000000..be00c12 --- /dev/null +++ b/src/modules/ChartFieldsEditor/ChartFieldsEditor.stories.tsx @@ -0,0 +1,66 @@ +import React, {useEffect, useState} from 'react'; +import type {Meta, StoryObj} from '@storybook/react'; +import {fn} from 'storybook/test'; +import {ChartFieldsEditor} from './ChartFieldsEditor'; +import { + fieldsEditorProps, + getInitialFormValues, +} from '../../widgets/DashboardCharts/story/fieldsData'; + +const meta: Meta = { + title: 'Modules/ChartFieldsEditor', + component: ChartFieldsEditor, + tags: ['autodocs'], + args: { + ...fieldsEditorProps, + formValues: getInitialFormValues('line'), + onChange: fn(), + onSubmit: fn(), + onCancel: fn(), + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + render: function Render(args) { + const [formValues, setFormValues] = useState(args.formValues); + useEffect(() => setFormValues(args.formValues), [args.formValues]); + return ( + { + setFormValues(value); + args.onChange?.(value); + }} + /> + ); + }, +}; +export default meta; +type Story = StoryObj; +export const Default: Story = {}; +export const Incomplete: Story = { + args: { + formValues: { + chartType: 'line', + dimensionAxisType: 'category', + measureItems: [{id: 'measure'}], + }, + }, +}; +export const EmptyData: Story = {args: {getChartData: () => undefined}}; +export const Disabled: Story = {args: {formProps: {disabled: true}}}; +export const Pie: Story = {args: {formValues: getInitialFormValues('pie')}}; +export const Narrow: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/src/modules/ClustersList/ClustersList.tsx b/src/modules/ClustersList/ClustersList.tsx index c40b57f..d0bed41 100644 --- a/src/modules/ClustersList/ClustersList.tsx +++ b/src/modules/ClustersList/ClustersList.tsx @@ -1,24 +1,14 @@ -import React from 'react'; import cn from 'bem-cn-lite'; -import type {NavigationCluster, RenderNavigationCluster} from '../../types/navigation'; +import React from 'react'; import {ClusterRow} from '../../components/ClusterRow'; import {LazyList} from '../../components/LazyList'; import {NAVIGATION_ROW_HEIGHT} from '../../constants/row'; +import type {ClustersListProps} from '../../types/clustersListProps'; +import type {NavigationCluster} from '../../types/navigation'; +export type {ClustersListProps} from '../../types/clustersListProps'; const block = cn('qp-clusters-list'); -export type ClustersListProps = { - items: T[]; - loading?: boolean; - error?: React.ReactNode; - hasMore?: boolean; - onLoadMore?: () => void; - emptyContent?: React.ReactNode; - renderRowItem?: RenderNavigationCluster; - onItemClick?: (cluster: T) => void; - className?: string; -}; - export const ClustersList = ({ items, loading, diff --git a/src/modules/HistoryComparisonActions/HistoryComparisonActions.tsx b/src/modules/HistoryComparisonActions/HistoryComparisonActions.tsx index 253af97..75db3fc 100644 --- a/src/modules/HistoryComparisonActions/HistoryComparisonActions.tsx +++ b/src/modules/HistoryComparisonActions/HistoryComparisonActions.tsx @@ -1,15 +1,12 @@ -import React from 'react'; -import {Button, Flex, Icon} from '@gravity-ui/uikit'; import ArrowRightArrowLeftIcon from '@gravity-ui/icons/svgs/arrow-right-arrow-left.svg'; import ArrowRotateRightIcon from '@gravity-ui/icons/svgs/arrow-rotate-right.svg'; -import {QueryListComparisonConfig, QueryListRow} from '../../types/queryList'; +import {Button, Flex, Icon} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React from 'react'; +import type {HistoryComparisonActionsProps} from '../../types/historyComparisonActionsProps'; +import {QueryListRow} from '../../types/queryList'; import './HistoryComparisonActions.scss'; - -export type HistoryComparisonActionsProps = { - comparison: QueryListComparisonConfig; - className?: string; -}; +export type {HistoryComparisonActionsProps} from '../../types/historyComparisonActionsProps'; const block = cn('qp-history-comparison-actions'); diff --git a/src/modules/HistoryHeader/HistoryHeader.tsx b/src/modules/HistoryHeader/HistoryHeader.tsx index 74d9222..96e831a 100644 --- a/src/modules/HistoryHeader/HistoryHeader.tsx +++ b/src/modules/HistoryHeader/HistoryHeader.tsx @@ -1,27 +1,15 @@ +import cn from 'bem-cn-lite'; import React, {FC, useEffect, useState} from 'react'; -import {FullSearchToggleButton} from './internal/FullSearchToggleButton'; import {HistoryFilter} from '../../components/HistoryFilter'; import {SearchWithButtons} from '../../components/SearchWithButtons'; -import type {QueryListFilterConfig} from '../../types/queryList'; -import i18n from './i18n'; -import cn from 'bem-cn-lite'; -import type {HistoryPanelVariant} from '../../types/listPanel'; +import type {HistoryHeaderProps as Props} from '../../types/historyHeaderProps'; import './HistoryHeader.scss'; +import i18n from './i18n'; +import {FullSearchToggleButton} from './internal/FullSearchToggleButton'; +export type {HistoryHeaderProps as Props} from '../../types/historyHeaderProps'; const block = cn('qp-history-header'); -type Props = { - search?: string; - fullSearch?: boolean; - fullSearchAvailable?: boolean; - hasClear?: boolean; - filter?: QueryListFilterConfig; - actions?: React.ReactNode; - onUpdate: (data: {value: string; fullSearch: boolean}) => void; - className?: string; - variant?: HistoryPanelVariant; -}; - export const HistoryHeader: FC = ({ search, fullSearch, diff --git a/src/modules/HistoryHeader/index.ts b/src/modules/HistoryHeader/index.ts index ab48ed6..6f4ed34 100644 --- a/src/modules/HistoryHeader/index.ts +++ b/src/modules/HistoryHeader/index.ts @@ -1 +1,3 @@ export {HistoryHeader} from './HistoryHeader'; + +export type {HistoryHeaderProps} from '../../types/historyHeaderProps'; diff --git a/src/modules/HistoryLayout/HistoryLayout.tsx b/src/modules/HistoryLayout/HistoryLayout.tsx index 9668577..7c4a487 100644 --- a/src/modules/HistoryLayout/HistoryLayout.tsx +++ b/src/modules/HistoryLayout/HistoryLayout.tsx @@ -1,22 +1,12 @@ -import React, {FC, ReactNode} from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import type {HistoryPanelVariant} from '../../types/listPanel'; +import React, {FC} from 'react'; +import type {HistoryLayoutProps} from '../../types/historyLayoutProps'; import './HistoryLayout.scss'; +export type {HistoryLayoutProps} from '../../types/historyLayoutProps'; const block = cn('qp-history-layout'); -export type HistoryLayoutProps = { - title?: ReactNode; - logo?: ReactNode; - actions?: ReactNode; - header?: ReactNode; - footer?: ReactNode; - className?: string; - variant?: HistoryPanelVariant; - children: ReactNode; -}; - export const HistoryLayout: FC = ({ title, logo, diff --git a/src/modules/HistoryRow/HistoryRow.tsx b/src/modules/HistoryRow/HistoryRow.tsx index f86fade..d10ff32 100644 --- a/src/modules/HistoryRow/HistoryRow.tsx +++ b/src/modules/HistoryRow/HistoryRow.tsx @@ -1,21 +1,18 @@ -import React from 'react'; -import type {QueryListRowRenderData} from '../../types/queryList'; -import type {QueryHistoryRow} from '../../types/history'; import {Flex, Text} from '@gravity-ui/uikit'; -import './HistoryRow.scss'; import cn from 'bem-cn-lite'; +import React from 'react'; import {EditableRowTitle} from '../../components/EditableRowTitle'; import {HistoryPrivateIcon} from '../../components/HistoryPrivateIcon'; import {QueryDuration} from '../../components/QueryDuration'; import {QueryStatusIcon} from '../../components/QueryStatusIcon'; import {RowActionsMenu} from '../../components/RowActionsMenu'; import {RowLink} from '../../components/RowLink'; -import {formatTime} from '../../helpers/time'; import {isFieldVisible} from '../../helpers/isFieldVisible'; - -export type Props = { - item: T; -} & Omit, 'item' | 'variant'>; +import {formatTime} from '../../helpers/time'; +import type {QueryHistoryRow} from '../../types/history'; +import type {HistoryRowProps as Props} from '../../types/historyRowProps'; +import './HistoryRow.scss'; +export type {HistoryRowProps as Props} from '../../types/historyRowProps'; const block = cn('qp-history-row'); diff --git a/src/modules/HistoryRow/index.ts b/src/modules/HistoryRow/index.ts index e19173d..ce54a79 100644 --- a/src/modules/HistoryRow/index.ts +++ b/src/modules/HistoryRow/index.ts @@ -1 +1,3 @@ export {HistoryRow} from './HistoryRow'; + +export type {HistoryRowProps} from '../../types/historyRowProps'; diff --git a/src/modules/HistorySearchRow/HistorySearchRow.tsx b/src/modules/HistorySearchRow/HistorySearchRow.tsx index 3b8a6ed..4035b9a 100644 --- a/src/modules/HistorySearchRow/HistorySearchRow.tsx +++ b/src/modules/HistorySearchRow/HistorySearchRow.tsx @@ -1,23 +1,20 @@ -import React from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; -import type {QueryListRowRenderData} from '../../types/queryList'; -import type {QueryHistoryRow} from '../../types/history'; +import cn from 'bem-cn-lite'; +import React from 'react'; import {HistoryPrivateIcon} from '../../components/HistoryPrivateIcon'; import {QueryDuration} from '../../components/QueryDuration'; import {QueryStatusIcon} from '../../components/QueryStatusIcon'; import {SearchRowLayout} from '../../components/SearchRowLayout'; -import {formatTimeCanonical} from '../../helpers/time'; import {isFieldVisible} from '../../helpers/isFieldVisible'; +import {formatTimeCanonical} from '../../helpers/time'; +import type {QueryHistoryRow} from '../../types/history'; +import type {HistorySearchRowProps as Props} from '../../types/historySearchRowProps'; import {resolveMonacoLanguage} from './helpers/resolveMonacoLanguage'; import './HistorySearchRow.scss'; -import cn from 'bem-cn-lite'; +export type {HistorySearchRowProps as Props} from '../../types/historySearchRowProps'; const block = cn('qp-history-search-row'); -export type Props = { - item: T; -} & Omit, 'item' | 'variant'>; - export const HistorySearchRow = ({ item, visibleFields, diff --git a/src/modules/HistorySearchRow/helpers/resolveMonacoLanguage.ts b/src/modules/HistorySearchRow/helpers/resolveMonacoLanguage.ts index 2f7ae5f..ead60a2 100644 --- a/src/modules/HistorySearchRow/helpers/resolveMonacoLanguage.ts +++ b/src/modules/HistorySearchRow/helpers/resolveMonacoLanguage.ts @@ -1,4 +1,4 @@ -import {MonacoLanguage} from '../../../components/MonacoEditor'; +import {MonacoLanguage} from '../../../components/MonacoEditor/monaco-yql-languages/constants'; export const resolveMonacoLanguage = (engine?: string) => { const normalized = engine?.toLowerCase(); diff --git a/src/modules/HistorySearchRow/index.ts b/src/modules/HistorySearchRow/index.ts index 00258db..29c5a90 100644 --- a/src/modules/HistorySearchRow/index.ts +++ b/src/modules/HistorySearchRow/index.ts @@ -1 +1,3 @@ export {HistorySearchRow} from './HistorySearchRow'; + +export type {HistorySearchRowProps} from '../../types/historySearchRowProps'; diff --git a/src/modules/NavigationDetail/NavigationDetail.stories.tsx b/src/modules/NavigationDetail/NavigationDetail.stories.tsx new file mode 100644 index 0000000..215ecbb --- /dev/null +++ b/src/modules/NavigationDetail/NavigationDetail.stories.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import {Button, Flex, Text} from '@gravity-ui/uikit'; +import type {Meta, StoryObj} from '@storybook/react'; +import type {NavigationDetailTab} from '../../types/navigation'; +import {NavigationDetail} from './NavigationDetail'; + +const meta: Meta = { + title: 'Modules/NavigationDetail', + component: NavigationDetail, + parameters: {layout: 'padded'}, +}; +export default meta; +type Story = StoryObj; + +const tabs: NavigationDetailTab[] = [ + {id: 'schema', title: 'Schema', content: 'Schema content'}, + {id: 'preview', title: 'Preview', content: 'Preview content'}, +]; + +export const Default: Story = { + args: { + config: {tabs}, + location: {cluster: 'demo', path: '//home/demo'}, + onUpdate: () => {}, + }, +}; + +function DynamicTabsExample() { + const [currentTabs, setCurrentTabs] = React.useState([]); + const [selection, setSelection] = React.useState('No user selection'); + return ( + + + + + + + + {}} + onTabUpdate={setSelection} + /> + User selection: {selection} + + ); +} + +/** Simulates loaded, hidden, removed and disabled tabs without remounting the detail. */ +export const DynamicTabs: Story = {render: () => }; diff --git a/src/modules/NavigationDetail/NavigationDetail.tsx b/src/modules/NavigationDetail/NavigationDetail.tsx index 7e0b7cd..ff7979a 100644 --- a/src/modules/NavigationDetail/NavigationDetail.tsx +++ b/src/modules/NavigationDetail/NavigationDetail.tsx @@ -3,14 +3,7 @@ import cn from 'bem-cn-lite'; import {Flex} from '@gravity-ui/uikit'; import {NavigationHeader} from '../NavigationHeader'; import {SearchWithButtons} from '../../components/SearchWithButtons'; -import type { - GetNavigationBreadcrumbHref, - NavigationDetailConfig, - NavigationHeaderAction, - NavigationLocation, - RenderNavigationHeaderActions, -} from '../../types/navigation'; -import type {LoadPathSuggestions} from '../../types/pathEditor'; +import type {NavigationDetailProps} from '../../types/navigationDetail'; import {NavigationDetailTabs} from './internal/NavigationDetailTabs'; import {getInitialTab} from './helpers/getInitialTab'; import {getVisibleTabs} from './helpers/getVisibleTabs'; @@ -18,21 +11,6 @@ import './NavigationDetail.scss'; const block = cn('qp-navigation-detail'); -export type NavigationDetailProps = { - config: NavigationDetailConfig; - location: NavigationLocation; - onUpdate: (location: NavigationLocation) => void; - onLoadSuggestions?: LoadPathSuggestions; - actions?: NavigationHeaderAction[]; - renderActions?: RenderNavigationHeaderActions; - getBreadcrumbHref?: GetNavigationBreadcrumbHref; - activeTab?: string; - onTabUpdate?: (tab: string) => void; - search?: string; - onSearchUpdate?: (value: string) => void; - className?: string; -}; - export const NavigationDetail: React.FC = ({ config, location, @@ -55,6 +33,13 @@ export const NavigationDetail: React.FC = ({ const [activeTabState, setActiveTabState] = useState(() => getInitialTab(visibleTabs, config.defaultTab), ); + if ( + !isTabControlled && + !visibleTabs.some((tab) => tab.id === activeTabState && !tab.disabled) + ) { + const nextTab = getInitialTab(visibleTabs, config.defaultTab); + if (nextTab !== activeTabState) setActiveTabState(nextTab); + } const [searchState, setSearchState] = useState(''); const activeTab = isTabControlled ? activeTabProp : activeTabState; @@ -92,7 +77,7 @@ export const NavigationDetail: React.FC = ({ null) : null; - const hasTabs = visibleTabs.length > 0; + const hasTabs = visibleTabs.some((tab) => !tab.disabled); return ( diff --git a/src/modules/NavigationDetail/helpers/getInitialTab.ts b/src/modules/NavigationDetail/helpers/getInitialTab.ts index 19730b9..7a7872d 100644 --- a/src/modules/NavigationDetail/helpers/getInitialTab.ts +++ b/src/modules/NavigationDetail/helpers/getInitialTab.ts @@ -2,8 +2,8 @@ import type {NavigationDetailTab} from '../../../types/navigation'; export function getInitialTab(visibleTabs: NavigationDetailTab[], defaultTab?: string): string { const defaultTabConfig = defaultTab - ? visibleTabs.find((tab) => tab.id === defaultTab) + ? visibleTabs.find((tab) => tab.id === defaultTab && !tab.disabled) : undefined; const firstEnabled = visibleTabs.find((tab) => !tab.disabled); - return (defaultTabConfig ?? firstEnabled ?? visibleTabs[0])?.id ?? ''; + return (defaultTabConfig ?? firstEnabled)?.id ?? ''; } diff --git a/src/modules/NavigationDetail/index.ts b/src/modules/NavigationDetail/index.ts index 9488a35..63073da 100644 --- a/src/modules/NavigationDetail/index.ts +++ b/src/modules/NavigationDetail/index.ts @@ -1,2 +1,2 @@ export {NavigationDetail} from './NavigationDetail'; -export type {NavigationDetailProps} from './NavigationDetail'; +export type {NavigationDetailProps} from '../../types/navigationDetail'; diff --git a/src/modules/NavigationHeader/NavigationHeader.tsx b/src/modules/NavigationHeader/NavigationHeader.tsx index c83469f..6af5759 100644 --- a/src/modules/NavigationHeader/NavigationHeader.tsx +++ b/src/modules/NavigationHeader/NavigationHeader.tsx @@ -1,24 +1,9 @@ -import React, {FC} from 'react'; import {Flex} from '@gravity-ui/uikit'; +import React, {FC} from 'react'; import {Breadcrumbs} from '../../components/Breadcrumbs'; import {NavigationActionButtons} from '../../components/NavigationActionButtons'; -import type { - GetNavigationBreadcrumbHref, - NavigationHeaderAction, - NavigationLocation, - RenderNavigationHeaderActions, -} from '../../types/navigation'; -import type {LoadPathSuggestions} from '../../types/pathEditor'; - -export type NavigationHeaderProps = { - location: NavigationLocation; - actions?: NavigationHeaderAction[]; - renderActions?: RenderNavigationHeaderActions; - getBreadcrumbHref?: GetNavigationBreadcrumbHref; - onUpdate: (location: NavigationLocation) => void; - onLoadSuggestions?: LoadPathSuggestions; - className?: string; -}; +import type {NavigationHeaderProps} from '../../types/navigationHeaderProps'; +export type {NavigationHeaderProps} from '../../types/navigationHeaderProps'; export const NavigationHeader: FC = ({ location, diff --git a/src/modules/NavigationItemsList/NavigationItemsList.tsx b/src/modules/NavigationItemsList/NavigationItemsList.tsx index 14b0f70..54988dc 100644 --- a/src/modules/NavigationItemsList/NavigationItemsList.tsx +++ b/src/modules/NavigationItemsList/NavigationItemsList.tsx @@ -1,39 +1,18 @@ -import React from 'react'; import cn from 'bem-cn-lite'; -import type { - NavigationItem, - NavigationParentRowConfig, - NavigationSortOrder, - RenderNavigationItem, -} from '../../types/navigation'; +import React from 'react'; import {LazyList} from '../../components/LazyList'; import {NavigationItemRow} from '../../components/NavigationItemRow'; -import {NavigationItemsListHeader} from './internal/NavigationItemsListHeader'; +import {NAVIGATION_ROW_HEIGHT} from '../../constants/row'; +import type {NavigationItem} from '../../types/navigation'; +import type {NavigationItemsListProps} from '../../types/navigationItemsListProps'; import {NavigationItemsListEmptyState} from './internal/NavigationItemsListEmptyState'; +import {NavigationItemsListHeader} from './internal/NavigationItemsListHeader'; import {useParentRow} from './internal/useParentRow'; -import {NAVIGATION_ROW_HEIGHT} from '../../constants/row'; import './NavigationItemsList.scss'; +export type {NavigationItemsListProps} from '../../types/navigationItemsListProps'; const block = cn('qp-navigation-items-list'); -export type NavigationItemsListProps = { - items: T[]; - path?: string; - search?: string; - parentRow?: NavigationParentRowConfig; - sort?: NavigationSortOrder; - onSortUpdate?: (sort: NavigationSortOrder) => void; - titleLabel: string; - loading?: boolean; - error?: React.ReactNode; - hasMore?: boolean; - onLoadMore?: () => void; - emptyContent?: React.ReactNode; - renderRowItem?: RenderNavigationItem; - onItemClick?: (item: T) => void; - className?: string; -}; - export const NavigationItemsList = ({ items, path, diff --git a/src/modules/NavigationMeta/NavigationMeta.tsx b/src/modules/NavigationMeta/NavigationMeta.tsx index 0c1f16f..92152b8 100644 --- a/src/modules/NavigationMeta/NavigationMeta.tsx +++ b/src/modules/NavigationMeta/NavigationMeta.tsx @@ -1,26 +1,17 @@ -import React, {useMemo} from 'react'; import {DefinitionList, Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {useMemo} from 'react'; import {EmptyContent} from '../../components/EmptyContent'; import {SkeletonRows} from '../../components/SkeletonRows'; -import type {NavigationMetaConfig, NavigationMetaItem} from '../../types/navigation'; +import type {NavigationMetaItem} from '../../types/navigation'; +import type {NavigationMetaProps} from '../../types/navigationMetaProps'; import {buildMetaGroups} from './helpers/buildMetaGroups'; import i18n from './i18n'; import './NavigationMeta.scss'; +export type {NavigationMetaProps, NavigationMetaViewConfig} from '../../types/navigationMetaProps'; const block = cn('qp-navigation-meta'); -export type NavigationMetaViewConfig = { - render?: (data: NavigationMetaConfig) => React.ReactNode; - extraContent?: React.ReactNode; -}; - -export type NavigationMetaProps = { - data: NavigationMetaConfig; - view?: NavigationMetaViewConfig; - className?: string; -}; - export function NavigationMeta({ data, view, diff --git a/src/modules/NavigationSchema/NavigationSchema.tsx b/src/modules/NavigationSchema/NavigationSchema.tsx index 13f9054..0392e3b 100644 --- a/src/modules/NavigationSchema/NavigationSchema.tsx +++ b/src/modules/NavigationSchema/NavigationSchema.tsx @@ -1,40 +1,22 @@ -import React, {useMemo} from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import type {Column} from '../../components/DataTable'; +import React, {useMemo} from 'react'; import {DataTable} from '../../components/DataTable'; import {FieldsSearchToolbar} from '../../components/FieldsSearchToolbar'; import {useVisibleColumns} from '../../helpers/useVisibleColumns'; -import type {NavigationSchemaColumn, NavigationSchemaConfig} from '../../types/navigation'; +import type {NavigationSchemaColumn} from '../../types/navigation'; +import type {NavigationSchemaProps} from '../../types/navigationSchemaProps'; import {buildSchemaColumns} from './helpers/buildSchemaColumns'; import {filterSchema} from './helpers/filterSchema'; import i18n from './i18n'; import './NavigationSchema.scss'; +export type { + NavigationSchemaProps, + NavigationSchemaViewConfig, +} from '../../types/navigationSchemaProps'; const block = cn('qp-navigation-schema'); -export type NavigationSchemaViewConfig< - TColumn extends NavigationSchemaColumn = NavigationSchemaColumn, -> = { - tableColumns?: Array>; - extraColumns?: Array>; -}; - -export type NavigationSchemaProps = - { - data: NavigationSchemaConfig; - view?: NavigationSchemaViewConfig; - search?: string; - onSearchUpdate?: (value: string) => void; - searchPlaceholder?: string; - visibleColumns?: string[]; - onVisibleColumnsChange?: (value: string[]) => void; - defaultVisibleColumns?: string[]; - hideToolbar?: boolean; - hideFieldsSelector?: boolean; - className?: string; - }; - export function NavigationSchema({ data, view, diff --git a/src/modules/NavigationView/NavigationView.tsx b/src/modules/NavigationView/NavigationView.tsx index 333f477..64d0585 100644 --- a/src/modules/NavigationView/NavigationView.tsx +++ b/src/modules/NavigationView/NavigationView.tsx @@ -1,31 +1,18 @@ -import React, {useState} from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; -import type {Column} from '../../components/DataTable'; +import React, {useState} from 'react'; import {EmptyContent} from '../../components/EmptyContent'; -import {SkeletonRows} from '../../components/SkeletonRows'; import {SearchWithButtons} from '../../components/SearchWithButtons'; -import type {NavigationViewConfig, NavigationViewRow} from '../../types/navigation'; -import {NavigationViewSectionItem} from './internal/NavigationViewSectionItem'; +import {SkeletonRows} from '../../components/SkeletonRows'; +import type {NavigationViewRow} from '../../types/navigation'; +import type {NavigationViewProps} from '../../types/navigationViewProps'; import i18n from './i18n'; +import {NavigationViewSectionItem} from './internal/NavigationViewSectionItem'; import './NavigationView.scss'; +export type {NavigationViewProps, NavigationViewViewConfig} from '../../types/navigationViewProps'; const block = cn('qp-navigation-view'); -export type NavigationViewViewConfig = { - tableColumns?: Array>; - extraColumns?: Array>; -}; - -export type NavigationViewProps = { - data: NavigationViewConfig; - view?: NavigationViewViewConfig; - search?: string; - onSearchUpdate?: (value: string) => void; - searchPlaceholder?: string; - className?: string; -}; - export function NavigationView({ data, view, diff --git a/src/modules/QueriesList/QueriesList.tsx b/src/modules/QueriesList/QueriesList.tsx index c6d8514..ced192a 100644 --- a/src/modules/QueriesList/QueriesList.tsx +++ b/src/modules/QueriesList/QueriesList.tsx @@ -1,26 +1,16 @@ -import React, {useState} from 'react'; import cn from 'bem-cn-lite'; +import React, {useState} from 'react'; +import {EmptyContent} from '../../components/EmptyContent'; import {FieldsSelector} from '../../components/FieldsSelector'; import {useListKey} from '../../helpers/useListKey'; -import type { - QueryListComparisonConfig, - QueryListEditingConfig, - QueryListFilterConfig, - QueryListItem, - QueryListLinkRenderer, - QueryListRow, - QueryListRowAction, - QueryListRowRenderData, - QueryListSearchConfig, - QueryListVisibleFieldsConfig, -} from '../../types/queryList'; +import type {QueriesListProps} from '../../types/queriesListProps'; +import type {QueryListRow, QueryListSearchConfig} from '../../types/queryList'; import {HistoryComparisonActions} from '../HistoryComparisonActions'; import {HistoryHeader} from '../HistoryHeader'; import {HistoryLayout} from '../HistoryLayout'; import {RowsList} from '../RowsList'; import './QueriesList.scss'; -import {EmptyContent} from '../../components/EmptyContent'; -import type {QueryListPanelOptions} from '../../types/listPanel'; +export type {QueriesListProps} from '../../types/queriesListProps'; const block = cn('qp-queries-list'); @@ -30,26 +20,6 @@ const getPanelEmptyContent = (hasSearchOrFilter: boolean, emptyContent: React.Re return emptyContent; }; -export type QueriesListProps = QueryListPanelOptions & { - className?: string; - title: React.ReactNode; - logo?: React.ReactNode; - search: QueryListSearchConfig; - filter?: QueryListFilterConfig; - items: QueryListItem[]; - selectedRowId?: T['id']; - editing?: QueryListEditingConfig; - comparison?: QueryListComparisonConfig; - visibleFields?: QueryListVisibleFieldsConfig; - getRowActions?: (item: T) => QueryListRowAction[]; - renderRow: (data: QueryListRowRenderData) => React.ReactNode; - onListItemClick?: (item: QueryListItem) => void; - hasMore?: boolean; - loading?: boolean; - onLoadMore?: () => void; - renderLink?: QueryListLinkRenderer; -}; - export const QueriesList = ({ title, logo, diff --git a/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx b/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx index 3523b07..2b8ce1b 100644 --- a/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx +++ b/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx @@ -1,3 +1,4 @@ +import type {CreateTableDetailConfigOptions} from '../../../types/navigationDetailResolvers'; import React from 'react'; import {NavigationMeta} from '../../../modules/NavigationMeta'; import {NavigationPreview} from '../../../modules/NavigationPreview'; @@ -7,62 +8,13 @@ import type { NavigationDetailConfig, NavigationDetailConfigFactory, NavigationItem, - NavigationMetaConfig, NavigationMetaItem, - NavigationPreviewConfig, NavigationPreviewRow, - NavigationPreviewViewConfig, NavigationSchemaColumn, - NavigationSchemaConfig, - NavigationViewConfig, NavigationViewRow, } from '../../../types/navigation'; import i18n from '../i18n'; -export type NavigationSchemaResolver< - TItem extends NavigationItem = NavigationItem, - TColumn extends NavigationSchemaColumn = NavigationSchemaColumn, -> = (item: TItem) => NavigationSchemaConfig | undefined; - -export type NavigationPreviewResolver< - TItem extends NavigationItem = NavigationItem, - TRow extends NavigationPreviewRow = NavigationPreviewRow, -> = (item: TItem) => NavigationPreviewConfig | undefined; - -export type NavigationPreviewViewResolver< - TItem extends NavigationItem = NavigationItem, - TRow extends NavigationPreviewRow = NavigationPreviewRow, -> = (item: TItem) => NavigationPreviewViewConfig | undefined; - -export type NavigationMetaResolver< - TItem extends NavigationItem = NavigationItem, - TMetaItem extends NavigationMetaItem = NavigationMetaItem, -> = (item: TItem) => NavigationMetaConfig | undefined; - -export type NavigationMetaRenderer = ( - data: NavigationMetaConfig, -) => React.ReactNode; - -export type NavigationViewResolver< - TItem extends NavigationItem = NavigationItem, - TRow extends NavigationViewRow = NavigationViewRow, -> = (item: TItem) => NavigationViewConfig | undefined; - -export type CreateTableDetailConfigOptions< - TItem extends NavigationItem = NavigationItem, - TColumn extends NavigationSchemaColumn = NavigationSchemaColumn, - TRow extends NavigationPreviewRow = NavigationPreviewRow, - TMetaItem extends NavigationMetaItem = NavigationMetaItem, - TViewRow extends NavigationViewRow = NavigationViewRow, -> = { - resolveSchema?: NavigationSchemaResolver; - resolvePreview?: NavigationPreviewResolver; - resolvePreviewView?: NavigationPreviewViewResolver; - resolveMeta?: NavigationMetaResolver; - renderMeta?: NavigationMetaRenderer; - resolveView?: NavigationViewResolver; -}; - export const createTableDetailConfig = < TItem extends NavigationItem = NavigationItem, TColumn extends NavigationSchemaColumn = NavigationSchemaColumn, diff --git a/src/modules/QueriesNavigation/index.ts b/src/modules/QueriesNavigation/index.ts index f3d8bd1..1fb9cdc 100644 --- a/src/modules/QueriesNavigation/index.ts +++ b/src/modules/QueriesNavigation/index.ts @@ -9,6 +9,6 @@ export type { NavigationPreviewViewResolver, NavigationSchemaResolver, NavigationViewResolver, -} from './helpers/createTableDetailConfig'; +} from '../../types/navigationDetailResolvers'; export {createEmptyDetailConfig} from './helpers/createEmptyDetailConfig'; export {createNavigationDetailResolver} from './helpers/createNavigationDetailResolver'; diff --git a/src/modules/QueryParameters/i18n/en.json b/src/modules/QueryParameters/i18n/en.json index 1f3b605..21d3d97 100644 --- a/src/modules/QueryParameters/i18n/en.json +++ b/src/modules/QueryParameters/i18n/en.json @@ -60,8 +60,7 @@ "action_editing-actions": "Editing actions", "action_reset-value": "Reset value", "action_add": "Add", - "value_elements": "{{count}} elements", - "value_element": "{{count}} element", + "value_elements": "Elements: {{count}}", "title_parameter-details": "Parameter name and type", "title_confirm-update": "Update parameters?", "context_confirm-update": "Updating will discard your unsaved edits after a successful refresh.", @@ -69,8 +68,7 @@ "action_collapse-panel": "Collapse parameters", "action_close-panel": "Close parameters", "action_open-calendar": "Open calendar", - "value_field": "{{count}} field", - "value_fields": "{{count}} fields", + "value_fields": "Fields: {{count}}", "field_precision": "Precision", "field_scale": "Scale", "alert_invalid-date": "Enter a valid date/time in the indicated format.", diff --git a/src/modules/QueryParameters/i18n/ru.json b/src/modules/QueryParameters/i18n/ru.json index fb88824..2c37f2e 100644 --- a/src/modules/QueryParameters/i18n/ru.json +++ b/src/modules/QueryParameters/i18n/ru.json @@ -61,7 +61,6 @@ "action_reset-value": "Сбросить значение", "action_add": "Добавить", "value_elements": "Элементов: {{count}}", - "value_element": "Элементов: {{count}}", "title_parameter-details": "Имя и тип параметра", "title_confirm-update": "Обновить параметры?", "context_confirm-update": "После успешного обновления несохранённые правки будут отменены.", @@ -69,8 +68,7 @@ "action_collapse-panel": "Свернуть параметры", "action_close-panel": "Закрыть параметры", "action_open-calendar": "Открыть календарь", - "value_field": "{{count}} поле", - "value_fields": "{{count}} полей", + "value_fields": "Полей: {{count}}", "field_precision": "Точность", "field_scale": "Масштаб", "alert_invalid-date": "Введите дату и время в указанном формате.", diff --git a/src/modules/QueryParameters/internal/CollectionValue.scss b/src/modules/QueryParameters/internal/CollectionValue.scss new file mode 100644 index 0000000..85671fb --- /dev/null +++ b/src/modules/QueryParameters/internal/CollectionValue.scss @@ -0,0 +1,14 @@ +@use './parameterValueLayout' as layout; + +.qp-parameter-value { + @include layout.cell; + &__collection_list { + // Center the 1px guide under the 12px chevron; keep a 16px nesting step. + margin-inline-start: calc(var(--g-spacing-3) / 2 - 0.5px); + padding-inline-start: calc(var(--g-spacing-4) - var(--g-spacing-3) / 2 - 0.5px); + border-inline-start: 1px solid var(--g-color-line-generic); + } + &__add { + align-self: flex-start; + } +} diff --git a/src/modules/QueryParameters/internal/CollectionValue.tsx b/src/modules/QueryParameters/internal/CollectionValue.tsx new file mode 100644 index 0000000..db20bcf --- /dev/null +++ b/src/modules/QueryParameters/internal/CollectionValue.tsx @@ -0,0 +1,124 @@ +import React, {useEffect, useRef} from 'react'; +import {Button, Flex, Icon} from '@gravity-ui/uikit'; +import TrashBinIcon from '@gravity-ui/icons/svgs/trash-bin.svg'; +import PlusIcon from '@gravity-ui/icons/svgs/plus.svg'; +import type { + QueryParameterEntry, + QueryParameterItem, + QueryParameterType, + QueryParameterValue, +} from '../../../types/queryParameters'; +import {createId, defaultValue} from '../helpers/parameters'; +import i18n from '../i18n'; +import type {RecursiveValueProps} from './parameterValueTypes'; +import cn from 'bem-cn-lite'; +import './CollectionValue.scss'; +import {childProps} from './parameterValueHelpers'; +import {DictEntry} from './DictEntry'; + +const block = cn('qp-parameter-value'); + +export function CollectionValue(props: RecursiveValueProps) { + const {ValueComponent, type, value, initialValue, editing, disabled, label, onChange} = props; + const root = useRef(null); + const addButton = useRef(null); + const focusNew = useRef(false); + useEffect(() => { + if (focusNew.current) { + const inputs = root.current?.querySelectorAll('input, textarea'); + inputs?.[inputs.length - 1]?.focus(); + focusNew.current = false; + } + }, [value]); + if (type.kind !== 'List' && type.kind !== 'Tuple' && type.kind !== 'Dict') return null; + const items = value as QueryParameterItem[]; + const baseline = Array.isArray(initialValue) ? initialValue : []; + const remove = (id: string) => { + onChange(items.filter((item) => item.id !== id)); + addButton.current?.focus(); + }; + return ( + + {items.map((item, index) => { + const initial = baseline.find(({id}) => id === item.id); + const update = (next: QueryParameterValue) => + onChange( + items.map((entry) => + entry.id === item.id ? {...entry, value: next} : entry, + ), + ); + let itemType: QueryParameterType | undefined; + if (type.kind === 'List') itemType = type.item; + if (type.kind === 'Tuple') itemType = type.items[index]; + return ( + +
+ {type.kind === 'Dict' ? ( + + ) : ( + itemType && ( + + ) + )} +
+ {editing && type.kind !== 'Tuple' && ( + + )} +
+ ); + })} + {editing && type.kind !== 'Tuple' && ( + + )} +
+ ); +} diff --git a/src/modules/QueryParameters/internal/DictEntry.scss b/src/modules/QueryParameters/internal/DictEntry.scss new file mode 100644 index 0000000..8cef415 --- /dev/null +++ b/src/modules/QueryParameters/internal/DictEntry.scss @@ -0,0 +1,6 @@ +@use './parameterValueLayout' as layout; + +.qp-parameter-value { + @include layout.cell; + @include layout.text; +} diff --git a/src/modules/QueryParameters/internal/DictEntry.tsx b/src/modules/QueryParameters/internal/DictEntry.tsx new file mode 100644 index 0000000..8566e21 --- /dev/null +++ b/src/modules/QueryParameters/internal/DictEntry.tsx @@ -0,0 +1,151 @@ +import React, {useState} from 'react'; +import {Flex, TextArea} from '@gravity-ui/uikit'; +import type { + QueryParameterEntry, + QueryParameterItem, + QueryParameterType, + QueryParameterValue, +} from '../../../types/queryParameters'; +import {defaultValue, isNumericType, isTextType} from '../helpers/parameters'; +import i18n from '../i18n'; +import type {RecursiveValueProps} from './parameterValueTypes'; +import cn from 'bem-cn-lite'; +import './DictEntry.scss'; +import {childProps} from './parameterValueHelpers'; +import {useChildValidity} from './useChildValidity'; +import {needsEscapes, useRepresentation} from './useRepresentation'; +import {ValueActions} from './ValueActions'; + +const block = cn('qp-parameter-value'); + +export function DictEntry( + props: RecursiveValueProps & { + type: Extract; + entry: QueryParameterEntry; + initial?: QueryParameterEntry; + index: number; + }, +) { + const {ValueComponent, type, entry, initial, index, value, onChange, editing, disabled} = props; + const entries = value as QueryParameterEntry[]; + const [generation, setGeneration] = useState(0); + const {pending, childValidity, clearChildren} = useChildValidity(props.onValidityChange); + const childrenProps = {...props, onValidityChange: childValidity}; + const update = (next: QueryParameterEntry) => + onChange(entries.map((item) => (item.id === entry.id ? next : item))); + const pairType: QueryParameterType = {kind: 'Tuple', items: [type.key, type.value]}; + const pair = [ + {id: 'key', value: entry.key}, + {id: 'value', value: entry.value}, + ]; + const pairProps = { + ...props, + header: undefined, + type: pairType, + value: pair, + path: `${props.path ?? '$'}/${entry.id}`, + label: `${props.label}: ${index + 1}`, + onChange: (next: QueryParameterValue) => { + const parts = next as QueryParameterItem[]; + update({...entry, key: parts[0].value, value: parts[1].value}); + }, + }; + const representation = useRepresentation(pairProps); + const paired = + typeof entry.key === 'string' && + typeof entry.value === 'string' && + (isTextType(type.key) || isNumericType(type.key)) && + (isTextType(type.value) || isNumericType(type.value)) && + !needsEscapes(type.key, entry.key) && + !needsEscapes(type.value, entry.value); + const actions = editing && ( + 0} + onRaw={() => representation.toggle('raw')} + onReset={() => { + representation.clear(); + clearChildren(); + setGeneration((current) => current + 1); + update({ + ...entry, + key: initial?.key ?? defaultValue(type.key), + value: initial?.value ?? defaultValue(type.value), + }); + }} + /> + ); + if (representation.mode === 'raw') + return ( + +