From 7cde1d499541587770a98c9ad0cc49a5b1f1af85 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Wed, 14 Oct 2026 17:21:44 +0300 Subject: [PATCH 01/10] refactor(types)!: centralize public component contracts --- .../AddChartButton/AddChartButton.tsx | 53 ++++---------- .../AttachmentList/AttachmentList.tsx | 29 ++------ .../helpers/objectLinkValidator.ts | 12 +--- .../helpers/stringRequiredValidator.ts | 3 +- src/components/AttachmentList/index.ts | 5 +- .../AttachmentItem/AttachmentItem.tsx | 11 +-- .../internal/AttachmentItem/index.ts | 2 +- .../EditAttachmentItem/EditAttachmentItem.tsx | 11 +-- .../EditAttachmentItem/EditLinkItem.tsx | 32 ++------- .../internal/EditAttachmentItem/FileEdit.tsx | 13 +--- .../internal/EditAttachmentItem/index.ts | 6 +- .../AttachmentListPlaceholder.tsx | 21 ++---- src/components/Breadcrumbs/Breadcrumbs.tsx | 21 ++---- src/components/Chart/Chart.tsx | 25 ++----- src/components/ClusterRow/ClusterRow.tsx | 9 +-- .../Dashboard/Dashboard.stories.tsx | 2 +- src/components/Dashboard/Dashboard.tsx | 2 +- .../Dashboard/helpers/createDefaultLayout.ts | 2 +- .../Dashboard/hooks/useDashboardConfig.ts | 2 +- src/components/Dashboard/index.ts | 6 +- .../Dashboard/internal/DashboardProvider.tsx | 2 +- src/components/DataTable/DataTable.tsx | 18 ++--- .../EditableRowTitle/EditableRowTitle.tsx | 11 ++- src/components/EmptyContent/EmptyContent.tsx | 16 ++--- .../FieldsSearchToolbar.tsx | 15 +--- .../FieldsSelector/FieldsSelector.tsx | 25 ++----- .../HistoryFilter/HistoryFilter.tsx | 4 +- src/components/HistoryFilter/index.ts | 1 + .../HistoryGroupHeader/HistoryGroupHeader.tsx | 8 +-- src/components/HistoryGroupHeader/index.ts | 2 + .../HistoryPrivateIcon/HistoryPrivateIcon.tsx | 14 ++-- src/components/HistoryPrivateIcon/index.ts | 2 + src/components/LazyList/LazyList.tsx | 20 +----- src/components/ListSpinner/ListSpinner.tsx | 8 +-- .../NavigationActionButtons.tsx | 13 +--- .../NavigationItemRow/NavigationItemRow.tsx | 11 ++- src/components/PathEditor/PathEditor.tsx | 34 ++------- .../QueryDuration/QueryDuration.tsx | 12 +--- src/components/QueryDuration/index.ts | 2 + .../QueryStatusIcon/QueryStatusIcon.tsx | 19 +++-- src/components/QueryStatusIcon/index.ts | 2 + .../RowActionsMenu/RowActionsMenu.tsx | 13 ++-- src/components/RowLink/RowLink.tsx | 11 +-- .../SearchRowLayout/SearchRowLayout.tsx | 15 +--- .../SearchWithButtons/SearchWithButtons.tsx | 17 ++--- .../SimpleForm/CheckboxGroupField.tsx | 10 +-- .../SimpleForm/RangeDatePickerField.tsx | 12 +--- src/components/SimpleForm/SelectField.tsx | 10 +-- src/components/SimpleForm/SimpleForm.tsx | 29 ++++---- src/components/SimpleForm/SwitchField.tsx | 7 +- .../SimpleForm/helpers/getInitialValues.ts | 4 +- src/components/SimpleForm/index.ts | 14 +++- src/components/SkeletonRows/SkeletonRows.tsx | 10 +-- src/index.ts | 71 ++++++++++++++++++- src/modules/Attachments/Attachments.tsx | 2 +- src/modules/Attachments/index.ts | 2 +- src/modules/ChartEditor/types.ts | 17 +---- src/modules/ClustersList/ClustersList.tsx | 18 ++--- .../HistoryComparisonActions.tsx | 13 ++-- src/modules/HistoryHeader/HistoryHeader.tsx | 22 ++---- src/modules/HistoryHeader/index.ts | 2 + src/modules/HistoryLayout/HistoryLayout.tsx | 16 +---- src/modules/HistoryRow/HistoryRow.tsx | 15 ++-- src/modules/HistoryRow/index.ts | 2 + .../HistorySearchRow/HistorySearchRow.tsx | 15 ++-- src/modules/HistorySearchRow/index.ts | 2 + .../NavigationDetail/NavigationDetail.tsx | 24 +------ src/modules/NavigationDetail/index.ts | 2 +- .../NavigationHeader/NavigationHeader.tsx | 21 +----- .../NavigationItemsList.tsx | 33 ++------- src/modules/NavigationMeta/NavigationMeta.tsx | 17 ++--- .../NavigationSchema/NavigationSchema.tsx | 32 ++------- src/modules/NavigationView/NavigationView.tsx | 25 ++----- src/modules/QueriesList/QueriesList.tsx | 40 ++--------- .../helpers/createTableDetailConfig.tsx | 50 +------------ src/modules/QueriesNavigation/index.ts | 2 +- src/modules/RowsList/RowsList.tsx | 37 ++-------- src/modules/SavedQueryRow/SavedQueryRow.tsx | 10 +-- .../SavedQuerySearchRow.tsx | 10 +-- src/modules/TutorialRow/TutorialRow.tsx | 14 ++-- .../TutorialSearchRow/TutorialSearchRow.tsx | 14 ++-- src/types/addChartButton.ts | 35 +++++++++ src/types/attachmentListPlaceholderProps.ts | 12 ++++ src/types/attachmentListProps.ts | 23 ++++++ src/types/attachments.ts | 8 ++- src/types/attachmentsEditor.ts | 61 ++++++++++++++++ src/types/breadcrumbsProps.ts | 11 +++ src/types/chartEditor.ts | 13 ++++ src/types/chartProps.ts | 17 +++++ src/types/clusterRowProps.ts | 5 ++ src/types/clustersListProps.ts | 14 ++++ .../Dashboard/types.ts => types/dashboard.ts} | 0 src/types/dashboardCharts.ts | 4 +- src/types/dataTableProps.ts | 9 +++ src/types/editableRowTitleProps.ts | 6 ++ src/types/emptyContentProps.ts | 8 +++ src/types/fieldsSearchToolbarProps.ts | 12 ++++ src/types/fieldsSelectorProps.ts | 15 ++++ src/types/historyComparisonActionsProps.ts | 6 ++ src/types/historyFilter.ts | 3 + src/types/historyGroupHeaderProps.ts | 3 + src/types/historyHeaderProps.ts | 15 ++++ src/types/historyLayoutProps.ts | 13 ++++ src/types/historyPrivateIconProps.ts | 3 + src/types/historyRowProps.ts | 6 ++ src/types/historySearchRowProps.ts | 6 ++ src/types/lazyListProps.ts | 17 +++++ src/types/listSpinnerProps.ts | 3 + src/types/navigationActionButtonsProps.ts | 10 +++ src/types/navigationDetail.ts | 23 ++++++ src/types/navigationDetailResolvers.ts | 56 +++++++++++++++ src/types/navigationHeaderProps.ts | 17 +++++ src/types/navigationItemRowProps.ts | 5 ++ src/types/navigationItemsListProps.ts | 25 +++++++ src/types/navigationMetaProps.ts | 13 ++++ src/types/navigationSchemaProps.ts | 24 +++++++ src/types/navigationViewProps.ts | 16 +++++ src/types/pathEditorProps.ts | 28 ++++++++ src/types/queriesListProps.ts | 34 +++++++++ src/types/queryDurationProps.ts | 8 +++ src/types/queryExecutionPanel.ts | 2 +- src/types/queryResults.ts | 2 +- src/types/queryStatusIconProps.ts | 6 ++ src/types/rowActionsMenuProps.ts | 6 ++ src/types/rowLinkProps.ts | 8 +++ src/types/rowsListProps.ts | 31 ++++++++ src/types/savedQueryRowProps.ts | 8 +++ src/types/savedQuerySearchRowProps.ts | 8 +++ src/types/searchRowLayoutProps.ts | 12 ++++ src/types/searchWithButtonsProps.ts | 13 ++++ src/types/simpleFormProps.ts | 43 +++++++++++ src/types/skeletonRowsProps.ts | 5 ++ src/types/tutorialRowProps.ts | 7 ++ src/types/tutorialSearchRowProps.ts | 7 ++ 134 files changed, 1090 insertions(+), 841 deletions(-) create mode 100644 src/types/addChartButton.ts create mode 100644 src/types/attachmentListPlaceholderProps.ts create mode 100644 src/types/attachmentListProps.ts create mode 100644 src/types/attachmentsEditor.ts create mode 100644 src/types/breadcrumbsProps.ts create mode 100644 src/types/chartProps.ts create mode 100644 src/types/clusterRowProps.ts create mode 100644 src/types/clustersListProps.ts rename src/{components/Dashboard/types.ts => types/dashboard.ts} (100%) create mode 100644 src/types/dataTableProps.ts create mode 100644 src/types/editableRowTitleProps.ts create mode 100644 src/types/emptyContentProps.ts create mode 100644 src/types/fieldsSearchToolbarProps.ts create mode 100644 src/types/fieldsSelectorProps.ts create mode 100644 src/types/historyComparisonActionsProps.ts create mode 100644 src/types/historyFilter.ts create mode 100644 src/types/historyGroupHeaderProps.ts create mode 100644 src/types/historyHeaderProps.ts create mode 100644 src/types/historyLayoutProps.ts create mode 100644 src/types/historyPrivateIconProps.ts create mode 100644 src/types/historyRowProps.ts create mode 100644 src/types/historySearchRowProps.ts create mode 100644 src/types/lazyListProps.ts create mode 100644 src/types/listSpinnerProps.ts create mode 100644 src/types/navigationActionButtonsProps.ts create mode 100644 src/types/navigationDetail.ts create mode 100644 src/types/navigationDetailResolvers.ts create mode 100644 src/types/navigationHeaderProps.ts create mode 100644 src/types/navigationItemRowProps.ts create mode 100644 src/types/navigationItemsListProps.ts create mode 100644 src/types/navigationMetaProps.ts create mode 100644 src/types/navigationSchemaProps.ts create mode 100644 src/types/navigationViewProps.ts create mode 100644 src/types/pathEditorProps.ts create mode 100644 src/types/queriesListProps.ts create mode 100644 src/types/queryDurationProps.ts create mode 100644 src/types/queryStatusIconProps.ts create mode 100644 src/types/rowActionsMenuProps.ts create mode 100644 src/types/rowLinkProps.ts create mode 100644 src/types/rowsListProps.ts create mode 100644 src/types/savedQueryRowProps.ts create mode 100644 src/types/savedQuerySearchRowProps.ts create mode 100644 src/types/searchRowLayoutProps.ts create mode 100644 src/types/searchWithButtonsProps.ts create mode 100644 src/types/simpleFormProps.ts create mode 100644 src/types/skeletonRowsProps.ts create mode 100644 src/types/tutorialRowProps.ts create mode 100644 src/types/tutorialSearchRowProps.ts diff --git a/src/components/AddChartButton/AddChartButton.tsx b/src/components/AddChartButton/AddChartButton.tsx index 2cee68d..04846f2 100644 --- a/src/components/AddChartButton/AddChartButton.tsx +++ b/src/components/AddChartButton/AddChartButton.tsx @@ -1,4 +1,14 @@ -import React, {type ReactNode, useMemo, useState} from 'react'; +import type { + AddChartButtonOption, + AddChartButtonProps, + DefaultAddChartButtonValue, +} from '../../types/addChartButton'; +export type { + AddChartButtonOption, + AddChartButtonProps, + DefaultAddChartButtonValue, +} from '../../types/addChartButton'; +import React, {useMemo, useState} from 'react'; import { ChartAreaStacked, ChartBar, @@ -9,31 +19,13 @@ 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 './AddChartButton.scss'; 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}, @@ -45,27 +37,6 @@ export const DEFAULT_ADD_CHART_BUTTON_OPTIONS: readonly AddChartButtonOption; - -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[], 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.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..b6640d4 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'; diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx index 4ca8d9d..4a4dfde 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'; 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.tsx b/src/components/HistoryFilter/HistoryFilter.tsx index 432f88f..e2c0b65 100644 --- a/src/components/HistoryFilter/HistoryFilter.tsx +++ b/src/components/HistoryFilter/HistoryFilter.tsx @@ -3,7 +3,7 @@ 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 i18n from './i18n'; @@ -11,7 +11,7 @@ import './HistoryFilter.scss'; const block = cn('qp-history-filter'); -export const HistoryFilter: FC = ({ +export const HistoryFilter: FC = ({ fields = [], values, initialValues, 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/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/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.tsx b/src/components/SearchRowLayout/SearchRowLayout.tsx index 3bee4b3..760016d 100644 --- a/src/components/SearchRowLayout/SearchRowLayout.tsx +++ b/src/components/SearchRowLayout/SearchRowLayout.tsx @@ -1,25 +1,16 @@ -import React, {FC, ReactNode} from 'react'; import {Flex} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React, {FC} from 'react'; +import type {SearchRowLayoutProps} from '../../types/searchRowLayoutProps'; import {MonacoEditor} from '../MonacoEditor'; 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'); -export type SearchRowLayoutProps = { - header: ReactNode; - query?: string; - language: string; - href?: string; - disabled?: boolean; - renderLink?: QueryListLinkRenderer; - className?: string; -}; - export const SearchRowLayout: FC = ({ header, query, 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/index.ts b/src/index.ts index 0edd24a..d00ad26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, + 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..aad33c6 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'; 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/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/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.tsx b/src/modules/NavigationDetail/NavigationDetail.tsx index 7e0b7cd..9ae1e68 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, 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/RowsList/RowsList.tsx b/src/modules/RowsList/RowsList.tsx index d48beac..49f38ae 100644 --- a/src/modules/RowsList/RowsList.tsx +++ b/src/modules/RowsList/RowsList.tsx @@ -1,43 +1,16 @@ -import React, {useMemo} from 'react'; import cn from 'bem-cn-lite'; -import type { - QueryListComparisonConfig, - QueryListEditingConfig, - QueryListItem, - QueryListLinkRenderer, - QueryListRow, - QueryListRowAction, - QueryListRowRenderData, - QueryListRowVariant, - QueryListVisibleFieldsConfig, -} from '../../types/queryList'; +import React, {useMemo} from 'react'; import {EmptyContent} from '../../components/EmptyContent'; import {LazyList} from '../../components/LazyList'; -import {prepareRowData} from './helpers/prepareRowData'; import {SEARCH_ROW_HEIGHT} from '../../constants/row'; +import type {QueryListItem, QueryListRow} from '../../types/queryList'; +import type {RowsListProps} from '../../types/rowsListProps'; +import {prepareRowData} from './helpers/prepareRowData'; import './RowsList.scss'; -import type {ListEmptyContentProps} from '../../types/listPanel'; +export type {RowsListProps} from '../../types/rowsListProps'; const block = cn('qp-rows-list'); -export type RowsListProps = ListEmptyContentProps & { - items: QueryListItem[]; - selectedRowId?: T['id']; - rowVariant?: QueryListRowVariant; - visibleFields?: QueryListVisibleFieldsConfig; - editing?: QueryListEditingConfig; - comparison?: QueryListComparisonConfig; - getRowActions?: (item: T) => QueryListRowAction[]; - renderRow: (data: QueryListRowRenderData) => React.ReactNode; - showFiltersHint?: boolean; - hasMore?: boolean; - loading?: boolean; - onLoadMore?: () => void; - renderLink?: QueryListLinkRenderer; - className?: string; - onItemClick?: (item: QueryListItem, index: number) => void; -}; - export const RowsList = ({ items, selectedRowId, diff --git a/src/modules/SavedQueryRow/SavedQueryRow.tsx b/src/modules/SavedQueryRow/SavedQueryRow.tsx index e6afde8..8fb4e53 100644 --- a/src/modules/SavedQueryRow/SavedQueryRow.tsx +++ b/src/modules/SavedQueryRow/SavedQueryRow.tsx @@ -1,22 +1,18 @@ -import React from 'react'; import {Avatar, Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React from 'react'; import {EditableRowTitle} from '../../components/EditableRowTitle'; import {RowActionsMenu} from '../../components/RowActionsMenu'; import {RowLink} from '../../components/RowLink'; import {isFieldVisible} from '../../helpers/isFieldVisible'; import {formatDateCanonical} from '../../helpers/time'; -import type {QueryListRowRenderData} from '../../types/queryList'; import type {SavedQuery} from '../../types/savedQueries'; +import type {SavedQueryRowProps} from '../../types/savedQueryRowProps'; import './SavedQueryRow.scss'; +export type {SavedQueryRowProps} from '../../types/savedQueryRowProps'; const block = cn('qp-saved-query-row'); -export type SavedQueryRowProps = { - item: T; - renderAuthor?: (item: T) => React.ReactNode; -} & Omit, 'item' | 'variant'>; - export const SavedQueryRow = ({ item, actions, diff --git a/src/modules/SavedQuerySearchRow/SavedQuerySearchRow.tsx b/src/modules/SavedQuerySearchRow/SavedQuerySearchRow.tsx index 61e705e..bfbae01 100644 --- a/src/modules/SavedQuerySearchRow/SavedQuerySearchRow.tsx +++ b/src/modules/SavedQuerySearchRow/SavedQuerySearchRow.tsx @@ -1,21 +1,17 @@ -import React from 'react'; import {Avatar, Flex, Text} from '@gravity-ui/uikit'; import cn from 'bem-cn-lite'; +import React from 'react'; import {MonacoLanguage} from '../../components/MonacoEditor'; import {SearchRowLayout} from '../../components/SearchRowLayout'; import {isFieldVisible} from '../../helpers/isFieldVisible'; import {formatDateCanonical} from '../../helpers/time'; -import type {QueryListRowRenderData} from '../../types/queryList'; import type {SavedQuery} from '../../types/savedQueries'; +import type {SavedQuerySearchRowProps} from '../../types/savedQuerySearchRowProps'; import './SavedQuerySearchRow.scss'; +export type {SavedQuerySearchRowProps} from '../../types/savedQuerySearchRowProps'; const block = cn('qp-saved-query-search-row'); -export type SavedQuerySearchRowProps = { - item: T; - renderAuthor?: (item: T) => React.ReactNode; -} & Omit, 'item' | 'variant'>; - export const SavedQuerySearchRow = ({ item, comparison, diff --git a/src/modules/TutorialRow/TutorialRow.tsx b/src/modules/TutorialRow/TutorialRow.tsx index 8567b64..3e6490c 100644 --- a/src/modules/TutorialRow/TutorialRow.tsx +++ b/src/modules/TutorialRow/TutorialRow.tsx @@ -1,18 +1,14 @@ -import React from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; -import type {QueryListLinkRenderer} from '../../types/queryList'; -import type {TutorialHistoryRow} from '../../types/tutorial'; -import {RowLink} from '../../components/RowLink'; import cn from 'bem-cn-lite'; +import React from 'react'; +import {RowLink} from '../../components/RowLink'; +import type {TutorialHistoryRow} from '../../types/tutorial'; +import type {TutorialRowProps} from '../../types/tutorialRowProps'; import './TutorialRow.scss'; +export type {TutorialRowProps} from '../../types/tutorialRowProps'; const block = cn('qp-tutorial-row'); -export type TutorialRowProps = { - item: T; - renderLink?: QueryListLinkRenderer; -}; - export const TutorialRow = ({ item, renderLink, diff --git a/src/modules/TutorialSearchRow/TutorialSearchRow.tsx b/src/modules/TutorialSearchRow/TutorialSearchRow.tsx index 8491353..3576227 100644 --- a/src/modules/TutorialSearchRow/TutorialSearchRow.tsx +++ b/src/modules/TutorialSearchRow/TutorialSearchRow.tsx @@ -1,19 +1,15 @@ -import React from 'react'; import {Flex, Text} from '@gravity-ui/uikit'; -import type {QueryListLinkRenderer} from '../../types/queryList'; -import type {TutorialHistoryRow} from '../../types/tutorial'; +import cn from 'bem-cn-lite'; +import React from 'react'; import {MonacoLanguage} from '../../components/MonacoEditor'; import {SearchRowLayout} from '../../components/SearchRowLayout'; +import type {TutorialHistoryRow} from '../../types/tutorial'; +import type {TutorialSearchRowProps} from '../../types/tutorialSearchRowProps'; import './TutorialSearchRow.scss'; -import cn from 'bem-cn-lite'; +export type {TutorialSearchRowProps} from '../../types/tutorialSearchRowProps'; const block = cn('qp-tutorial-search-row'); -export type TutorialSearchRowProps = { - item: T; - renderLink?: QueryListLinkRenderer; -}; - export const TutorialSearchRow = ({ item, renderLink, diff --git a/src/types/addChartButton.ts b/src/types/addChartButton.ts new file mode 100644 index 0000000..65532f8 --- /dev/null +++ b/src/types/addChartButton.ts @@ -0,0 +1,35 @@ +import type {ReactNode} from 'react'; +import type {ButtonButtonProps, DropdownMenuProps, IconData} from '@gravity-ui/uikit'; + +export type AddChartButtonOption = { + value: TValue; + text: ReactNode; + icon?: IconData; + disabled?: boolean; + hidden?: boolean; +}; + +export type DefaultAddChartButtonValue = + 'Area' | 'BarX' | 'BarY' | 'Line' | 'Pie' | 'Scatter' | 'Waterfall'; + +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; +}; diff --git a/src/types/attachmentListPlaceholderProps.ts b/src/types/attachmentListPlaceholderProps.ts new file mode 100644 index 0000000..1caba58 --- /dev/null +++ b/src/types/attachmentListPlaceholderProps.ts @@ -0,0 +1,12 @@ +import type {ReactNode} from 'react'; + +export type AttachmentListPlaceholderProps = { + title?: ReactNode; + description?: ReactNode; + linkForDoc?: string; + linkText?: string; + className?: string; + onAddFile?: () => void; + onAddLink?: () => void; + qa?: string; +}; diff --git a/src/types/attachmentListProps.ts b/src/types/attachmentListProps.ts new file mode 100644 index 0000000..964c14b --- /dev/null +++ b/src/types/attachmentListProps.ts @@ -0,0 +1,23 @@ +import type {ListProps} from '@gravity-ui/uikit'; +import type {ReactNode} from 'react'; +import type {Attachment} from './attachments'; + +export type AttachmentListProps = Omit< + ListProps, + 'items' | 'selectedItemIndex' | 'virtualized' +> & { + attachments: Attachment[]; + className?: string; + onDelete?: (attachment: Attachment) => void; + onEdit?: (attachment: Attachment) => void; + onRevert?: (attachment: Attachment) => void; + + renderEditForm?: (attachment: Attachment) => ReactNode; + + wasAddedIds?: string[]; + wasEditedIds?: string[]; + + editingIds?: string[]; + + isDeleted?: boolean; +}; diff --git a/src/types/attachments.ts b/src/types/attachments.ts index 2add4c8..45cd3fe 100644 --- a/src/types/attachments.ts +++ b/src/types/attachments.ts @@ -1,7 +1,9 @@ -import type {AttachmentItemProps, AttachmentListProps} from '../components/AttachmentList'; -import type {AttachmentListPlaceholderProps} from '../components/AttachmentListPlaceholder'; +import type {AttachmentListProps} from './attachmentListProps'; +import type {AttachmentListPlaceholderProps} from './attachmentListPlaceholderProps'; -export type AttachItem = AttachmentItemProps['attachment'] & {token?: string; isNew?: boolean}; +export type Attachment = {id: string; name: string; link?: string}; + +export type AttachItem = Attachment & {token?: string; isNew?: boolean}; export type AttachmentsProps = { tokens?: {value: string; title: string}[]; diff --git a/src/types/attachmentsEditor.ts b/src/types/attachmentsEditor.ts new file mode 100644 index 0000000..1906d75 --- /dev/null +++ b/src/types/attachmentsEditor.ts @@ -0,0 +1,61 @@ +import type {Attachment} from './attachments'; + +export type AttachmentItemProps = { + attachment: Attachment; + wasEdited?: boolean; + wasAdded?: boolean; + onEdit?: (attachment: AttachmentItemProps['attachment']) => void; + onDelete?: (attachment: AttachmentItemProps['attachment']) => void; + onRevert?: (attachment: AttachmentItemProps['attachment']) => void; + isDeleted?: boolean; +}; + +export type EditLinkValues = { + 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; +}; + +export type EditFileItemProps = { + fileName?: string; + fileLabel?: string; + defaultFileName?: string; + onChangeFileName?: (fileName: string) => void; + onAccept?: (newFileName: string) => void; + onCancel?: () => void; + validator?: Validator; +}; + +export type EditAttachmentItemProps = + | (EditFileItemProps & { + type: 'file'; + }) + | (EditLinkItemProps & { + type: 'link'; + }); + +export type EditLinkErrors = Partial<{ + link: string; + token: string; + name: string; +}>; + +export type ObjectLinkValidator = ( + values: Record, + isTokenRequired?: boolean, +) => EditLinkErrors | null; + +export type Validator = (value: unknown) => string | null; diff --git a/src/types/breadcrumbsProps.ts b/src/types/breadcrumbsProps.ts new file mode 100644 index 0000000..222f557 --- /dev/null +++ b/src/types/breadcrumbsProps.ts @@ -0,0 +1,11 @@ +import type {GetNavigationBreadcrumbHref, NavigationLocation} from './navigation'; +import type {LoadPathSuggestions} from './pathEditor'; + +export type BreadcrumbsProps = { + location: NavigationLocation; + hideResetButton?: boolean; + className?: string; + onUpdate: (location: NavigationLocation) => void; + getBreadcrumbHref?: GetNavigationBreadcrumbHref; + onLoadSuggestions?: LoadPathSuggestions; +}; diff --git a/src/types/chartEditor.ts b/src/types/chartEditor.ts index f36a6a3..712e746 100644 --- a/src/types/chartEditor.ts +++ b/src/types/chartEditor.ts @@ -184,3 +184,16 @@ export type ChartFieldsEditorProps = Pick< emptyDataLabel?: string; formProps?: Pick; }; + +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; +}; diff --git a/src/types/chartProps.ts b/src/types/chartProps.ts new file mode 100644 index 0000000..713a8ac --- /dev/null +++ b/src/types/chartProps.ts @@ -0,0 +1,17 @@ +import type {ChartProps as GravityChartProps} from '@gravity-ui/charts'; +import type React from 'react'; + +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; +}; diff --git a/src/types/clusterRowProps.ts b/src/types/clusterRowProps.ts new file mode 100644 index 0000000..f930523 --- /dev/null +++ b/src/types/clusterRowProps.ts @@ -0,0 +1,5 @@ +import type {NavigationCluster} from './navigation'; + +export type ClusterRowProps = { + cluster: NavigationCluster; +}; diff --git a/src/types/clustersListProps.ts b/src/types/clustersListProps.ts new file mode 100644 index 0000000..0206fcd --- /dev/null +++ b/src/types/clustersListProps.ts @@ -0,0 +1,14 @@ +import type React from 'react'; +import type {NavigationCluster, RenderNavigationCluster} from './navigation'; + +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; +}; diff --git a/src/components/Dashboard/types.ts b/src/types/dashboard.ts similarity index 100% rename from src/components/Dashboard/types.ts rename to src/types/dashboard.ts diff --git a/src/types/dashboardCharts.ts b/src/types/dashboardCharts.ts index 61e3fa7..3c69181 100644 --- a/src/types/dashboardCharts.ts +++ b/src/types/dashboardCharts.ts @@ -1,9 +1,9 @@ import type {FlexProps} from '@gravity-ui/uikit'; import type {ConfigLayout} from '@gravity-ui/dashkit'; -import type {DashboardProps} from '../components/Dashboard'; -import type {ChartEditorProps} from '../modules/ChartEditor'; +import type {DashboardProps} from './dashboard'; import type { ChartData, + ChartEditorProps, ChartFieldsChartType, ChartFieldsEditorProps, ChartFieldsFormValues, diff --git a/src/types/dataTableProps.ts b/src/types/dataTableProps.ts new file mode 100644 index 0000000..2795a78 --- /dev/null +++ b/src/types/dataTableProps.ts @@ -0,0 +1,9 @@ +import type {DataTableProps as BaseDataTableProps} from '@gravity-ui/react-data-table'; +import type {EmptyContentVariant} from './emptyContentProps'; + +export type DataTableProps = { + loading?: boolean; + loaded?: boolean; + className?: string; + emptyVariant?: EmptyContentVariant; +} & Omit, 'theme'>; diff --git a/src/types/editableRowTitleProps.ts b/src/types/editableRowTitleProps.ts new file mode 100644 index 0000000..5509047 --- /dev/null +++ b/src/types/editableRowTitleProps.ts @@ -0,0 +1,6 @@ +import type {QueryListEditingRenderData, QueryListRow} from './queryList'; + +export type EditableRowTitleProps = { + item: T; + editing?: QueryListEditingRenderData; +}; diff --git a/src/types/emptyContentProps.ts b/src/types/emptyContentProps.ts new file mode 100644 index 0000000..423bc26 --- /dev/null +++ b/src/types/emptyContentProps.ts @@ -0,0 +1,8 @@ +import type {EmptyContentTextOverrides} from './listPanel'; + +export type EmptyContentVariant = 'no-files' | 'no-clusters' | 'nothing-found' | 'no-data'; + +export type EmptyContentProps = EmptyContentTextOverrides & { + variant: EmptyContentVariant; + className?: string; +}; diff --git a/src/types/fieldsSearchToolbarProps.ts b/src/types/fieldsSearchToolbarProps.ts new file mode 100644 index 0000000..930fa8d --- /dev/null +++ b/src/types/fieldsSearchToolbarProps.ts @@ -0,0 +1,12 @@ +import type {FieldsSelectorOption} from './fieldsSelectorProps'; + +export type FieldsSearchToolbarProps = { + search?: string; + onSearchUpdate?: (value: string) => void; + searchPlaceholder?: string; + fields: FieldsSelectorOption[]; + visibleFields: K[]; + onVisibleFieldsChange: (value: K[]) => void; + hideFieldsSelector?: boolean; + className?: string; +}; diff --git a/src/types/fieldsSelectorProps.ts b/src/types/fieldsSelectorProps.ts new file mode 100644 index 0000000..7c7eb39 --- /dev/null +++ b/src/types/fieldsSelectorProps.ts @@ -0,0 +1,15 @@ +import type {ButtonProps} from '@gravity-ui/uikit'; +import type {ReactNode} from 'react'; + +export type FieldsSelectorOption = { + id: K; + title: ReactNode; +}; + +export type FieldsSelectorProps = { + fields: FieldsSelectorOption[]; + value: K[]; + onChange: (value: K[]) => void; + buttonLabel?: string; + buttonView?: ButtonProps['view']; +}; diff --git a/src/types/historyComparisonActionsProps.ts b/src/types/historyComparisonActionsProps.ts new file mode 100644 index 0000000..d214e48 --- /dev/null +++ b/src/types/historyComparisonActionsProps.ts @@ -0,0 +1,6 @@ +import type {QueryListComparisonConfig, QueryListRow} from './queryList'; + +export type HistoryComparisonActionsProps = { + comparison: QueryListComparisonConfig; + className?: string; +}; diff --git a/src/types/historyFilter.ts b/src/types/historyFilter.ts new file mode 100644 index 0000000..1c55929 --- /dev/null +++ b/src/types/historyFilter.ts @@ -0,0 +1,3 @@ +import type {QueryListFilterConfig} from './queryList'; + +export type HistoryFilterProps = QueryListFilterConfig & {buttonView?: 'normal' | 'flat'}; diff --git a/src/types/historyGroupHeaderProps.ts b/src/types/historyGroupHeaderProps.ts new file mode 100644 index 0000000..5010ed7 --- /dev/null +++ b/src/types/historyGroupHeaderProps.ts @@ -0,0 +1,3 @@ +export type HistoryGroupHeaderProps = { + title: string; +}; diff --git a/src/types/historyHeaderProps.ts b/src/types/historyHeaderProps.ts new file mode 100644 index 0000000..2319e79 --- /dev/null +++ b/src/types/historyHeaderProps.ts @@ -0,0 +1,15 @@ +import type React from 'react'; +import type {HistoryPanelVariant} from './listPanel'; +import type {QueryListFilterConfig} from './queryList'; + +export type HistoryHeaderProps = { + search?: string; + fullSearch?: boolean; + fullSearchAvailable?: boolean; + hasClear?: boolean; + filter?: QueryListFilterConfig; + actions?: React.ReactNode; + onUpdate: (data: {value: string; fullSearch: boolean}) => void; + className?: string; + variant?: HistoryPanelVariant; +}; diff --git a/src/types/historyLayoutProps.ts b/src/types/historyLayoutProps.ts new file mode 100644 index 0000000..c1ee0fe --- /dev/null +++ b/src/types/historyLayoutProps.ts @@ -0,0 +1,13 @@ +import type {ReactNode} from 'react'; +import type {HistoryPanelVariant} from './listPanel'; + +export type HistoryLayoutProps = { + title?: ReactNode; + logo?: ReactNode; + actions?: ReactNode; + header?: ReactNode; + footer?: ReactNode; + className?: string; + variant?: HistoryPanelVariant; + children: ReactNode; +}; diff --git a/src/types/historyPrivateIconProps.ts b/src/types/historyPrivateIconProps.ts new file mode 100644 index 0000000..7b08765 --- /dev/null +++ b/src/types/historyPrivateIconProps.ts @@ -0,0 +1,3 @@ +export type HistoryPrivateIconProps = { + isPrivate?: boolean; +}; diff --git a/src/types/historyRowProps.ts b/src/types/historyRowProps.ts new file mode 100644 index 0000000..6e05ca1 --- /dev/null +++ b/src/types/historyRowProps.ts @@ -0,0 +1,6 @@ +import type {QueryHistoryRow} from './history'; +import type {QueryListRowRenderData} from './queryList'; + +export type HistoryRowProps = { + item: T; +} & Omit, 'item' | 'variant'>; diff --git a/src/types/historySearchRowProps.ts b/src/types/historySearchRowProps.ts new file mode 100644 index 0000000..10e11af --- /dev/null +++ b/src/types/historySearchRowProps.ts @@ -0,0 +1,6 @@ +import type {QueryHistoryRow} from './history'; +import type {QueryListRowRenderData} from './queryList'; + +export type HistorySearchRowProps = { + item: T; +} & Omit, 'item' | 'variant'>; diff --git a/src/types/lazyListProps.ts b/src/types/lazyListProps.ts new file mode 100644 index 0000000..91e0ecf --- /dev/null +++ b/src/types/lazyListProps.ts @@ -0,0 +1,17 @@ +import type React from 'react'; + +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; +}; diff --git a/src/types/listSpinnerProps.ts b/src/types/listSpinnerProps.ts new file mode 100644 index 0000000..45ae56c --- /dev/null +++ b/src/types/listSpinnerProps.ts @@ -0,0 +1,3 @@ +export type ListSpinnerProps = { + className?: string; +}; diff --git a/src/types/navigationActionButtonsProps.ts b/src/types/navigationActionButtonsProps.ts new file mode 100644 index 0000000..9fc28ef --- /dev/null +++ b/src/types/navigationActionButtonsProps.ts @@ -0,0 +1,10 @@ +import type {ButtonProps} from '@gravity-ui/uikit'; +import type {NavigationAction} from './navigation'; + +export type NavigationActionButtonsProps = { + actions?: Array>; + arg: TArg; + className?: string; + buttonClassName?: string; + size?: ButtonProps['size']; +}; diff --git a/src/types/navigationDetail.ts b/src/types/navigationDetail.ts new file mode 100644 index 0000000..c9efdda --- /dev/null +++ b/src/types/navigationDetail.ts @@ -0,0 +1,23 @@ +import type { + GetNavigationBreadcrumbHref, + NavigationDetailConfig, + NavigationHeaderAction, + NavigationLocation, + RenderNavigationHeaderActions, +} from './navigation'; +import type {LoadPathSuggestions} from './pathEditor'; + +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; +}; diff --git a/src/types/navigationDetailResolvers.ts b/src/types/navigationDetailResolvers.ts new file mode 100644 index 0000000..41b5722 --- /dev/null +++ b/src/types/navigationDetailResolvers.ts @@ -0,0 +1,56 @@ +import type React from 'react'; +import type { + NavigationItem, + NavigationMetaConfig, + NavigationMetaItem, + NavigationPreviewConfig, + NavigationPreviewRow, + NavigationPreviewViewConfig, + NavigationSchemaColumn, + NavigationSchemaConfig, + NavigationViewConfig, + NavigationViewRow, +} from './navigation'; +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; +}; diff --git a/src/types/navigationHeaderProps.ts b/src/types/navigationHeaderProps.ts new file mode 100644 index 0000000..f949269 --- /dev/null +++ b/src/types/navigationHeaderProps.ts @@ -0,0 +1,17 @@ +import type { + GetNavigationBreadcrumbHref, + NavigationHeaderAction, + NavigationLocation, + RenderNavigationHeaderActions, +} from './navigation'; +import type {LoadPathSuggestions} from './pathEditor'; + +export type NavigationHeaderProps = { + location: NavigationLocation; + actions?: NavigationHeaderAction[]; + renderActions?: RenderNavigationHeaderActions; + getBreadcrumbHref?: GetNavigationBreadcrumbHref; + onUpdate: (location: NavigationLocation) => void; + onLoadSuggestions?: LoadPathSuggestions; + className?: string; +}; diff --git a/src/types/navigationItemRowProps.ts b/src/types/navigationItemRowProps.ts new file mode 100644 index 0000000..82adb54 --- /dev/null +++ b/src/types/navigationItemRowProps.ts @@ -0,0 +1,5 @@ +import type {NavigationItem} from './navigation'; + +export type NavigationItemRowProps = { + item: NavigationItem; +}; diff --git a/src/types/navigationItemsListProps.ts b/src/types/navigationItemsListProps.ts new file mode 100644 index 0000000..98ca4d0 --- /dev/null +++ b/src/types/navigationItemsListProps.ts @@ -0,0 +1,25 @@ +import type React from 'react'; +import type { + NavigationItem, + NavigationParentRowConfig, + NavigationSortOrder, + RenderNavigationItem, +} from './navigation'; + +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; +}; diff --git a/src/types/navigationMetaProps.ts b/src/types/navigationMetaProps.ts new file mode 100644 index 0000000..3bb0b3e --- /dev/null +++ b/src/types/navigationMetaProps.ts @@ -0,0 +1,13 @@ +import type React from 'react'; +import type {NavigationMetaConfig, NavigationMetaItem} from './navigation'; + +export type NavigationMetaViewConfig = { + render?: (data: NavigationMetaConfig) => React.ReactNode; + extraContent?: React.ReactNode; +}; + +export type NavigationMetaProps = { + data: NavigationMetaConfig; + view?: NavigationMetaViewConfig; + className?: string; +}; diff --git a/src/types/navigationSchemaProps.ts b/src/types/navigationSchemaProps.ts new file mode 100644 index 0000000..3d8b9f6 --- /dev/null +++ b/src/types/navigationSchemaProps.ts @@ -0,0 +1,24 @@ +import type {Column} from '@gravity-ui/react-data-table'; +import type {NavigationSchemaColumn, NavigationSchemaConfig} from './navigation'; + +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; + }; diff --git a/src/types/navigationViewProps.ts b/src/types/navigationViewProps.ts new file mode 100644 index 0000000..febe182 --- /dev/null +++ b/src/types/navigationViewProps.ts @@ -0,0 +1,16 @@ +import type {Column} from '@gravity-ui/react-data-table'; +import type {NavigationViewConfig, NavigationViewRow} from './navigation'; + +export type NavigationViewViewConfig = { + tableColumns?: Array>; + extraColumns?: Array>; +}; + +export type NavigationViewProps = { + data: NavigationViewConfig; + view?: NavigationViewViewConfig; + search?: string; + onSearchUpdate?: (value: string) => void; + searchPlaceholder?: string; + className?: string; +}; diff --git a/src/types/pathEditorProps.ts b/src/types/pathEditorProps.ts new file mode 100644 index 0000000..8e14af6 --- /dev/null +++ b/src/types/pathEditorProps.ts @@ -0,0 +1,28 @@ +import type {FocusEvent} from 'react'; +import type { + LoadPathSuggestions, + PathEditorEventPayload, + PathEditorSuggestion, + PathEditorSuggestionFilter, +} from './pathEditor'; + +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; +}; diff --git a/src/types/queriesListProps.ts b/src/types/queriesListProps.ts new file mode 100644 index 0000000..9e4ce2b --- /dev/null +++ b/src/types/queriesListProps.ts @@ -0,0 +1,34 @@ +import type React from 'react'; +import type {QueryListPanelOptions} from './listPanel'; +import type { + QueryListComparisonConfig, + QueryListEditingConfig, + QueryListFilterConfig, + QueryListItem, + QueryListLinkRenderer, + QueryListRow, + QueryListRowAction, + QueryListRowRenderData, + QueryListSearchConfig, + QueryListVisibleFieldsConfig, +} from './queryList'; + +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; +}; diff --git a/src/types/queryDurationProps.ts b/src/types/queryDurationProps.ts new file mode 100644 index 0000000..0b95594 --- /dev/null +++ b/src/types/queryDurationProps.ts @@ -0,0 +1,8 @@ +import type {QueryStatus} from './history'; + +export type QueryDurationProps = { + startTime?: string | number; + endTime?: string | number; + status: QueryStatus; + className?: string; +}; diff --git a/src/types/queryExecutionPanel.ts b/src/types/queryExecutionPanel.ts index 51de57b..c45db01 100644 --- a/src/types/queryExecutionPanel.ts +++ b/src/types/queryExecutionPanel.ts @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import type {NavigationMetaProps} from '../modules/NavigationMeta'; +import type {NavigationMetaProps} from './navigationMetaProps'; import type {DashboardChartsProps} from './dashboardCharts'; import type {ErrorTreeProps} from './errorTree'; import type {NavigationMetaItem} from './navigation'; diff --git a/src/types/queryResults.ts b/src/types/queryResults.ts index f33189c..14fcdd1 100644 --- a/src/types/queryResults.ts +++ b/src/types/queryResults.ts @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import type {EmptyContentVariant} from '../components/EmptyContent'; +import type {EmptyContentVariant} from './emptyContentProps'; import type {AlignType, Settings} from '@gravity-ui/react-data-table'; export type QueryResultDataTypeParameter = diff --git a/src/types/queryStatusIconProps.ts b/src/types/queryStatusIconProps.ts new file mode 100644 index 0000000..3eb3506 --- /dev/null +++ b/src/types/queryStatusIconProps.ts @@ -0,0 +1,6 @@ +import type {QueryStatus} from './history'; + +export type QueryStatusIconProps = { + status: QueryStatus; + className?: string; +}; diff --git a/src/types/rowActionsMenuProps.ts b/src/types/rowActionsMenuProps.ts new file mode 100644 index 0000000..8f6a1e2 --- /dev/null +++ b/src/types/rowActionsMenuProps.ts @@ -0,0 +1,6 @@ +import type {QueryListRow, QueryListRowAction} from './queryList'; + +export type RowActionsMenuProps = { + item: T; + actions?: QueryListRowAction[]; +}; diff --git a/src/types/rowLinkProps.ts b/src/types/rowLinkProps.ts new file mode 100644 index 0000000..7eff236 --- /dev/null +++ b/src/types/rowLinkProps.ts @@ -0,0 +1,8 @@ +import type React from 'react'; +import type {QueryListLinkRenderer} from './queryList'; + +export type RowLinkProps = Omit, 'href'> & { + href?: string; + disabled?: boolean; + renderLink?: QueryListLinkRenderer; +}; diff --git a/src/types/rowsListProps.ts b/src/types/rowsListProps.ts new file mode 100644 index 0000000..b0addd5 --- /dev/null +++ b/src/types/rowsListProps.ts @@ -0,0 +1,31 @@ +import type React from 'react'; +import type {ListEmptyContentProps} from './listPanel'; +import type { + QueryListComparisonConfig, + QueryListEditingConfig, + QueryListItem, + QueryListLinkRenderer, + QueryListRow, + QueryListRowAction, + QueryListRowRenderData, + QueryListRowVariant, + QueryListVisibleFieldsConfig, +} from './queryList'; + +export type RowsListProps = ListEmptyContentProps & { + items: QueryListItem[]; + selectedRowId?: T['id']; + rowVariant?: QueryListRowVariant; + visibleFields?: QueryListVisibleFieldsConfig; + editing?: QueryListEditingConfig; + comparison?: QueryListComparisonConfig; + getRowActions?: (item: T) => QueryListRowAction[]; + renderRow: (data: QueryListRowRenderData) => React.ReactNode; + showFiltersHint?: boolean; + hasMore?: boolean; + loading?: boolean; + onLoadMore?: () => void; + renderLink?: QueryListLinkRenderer; + className?: string; + onItemClick?: (item: QueryListItem, index: number) => void; +}; diff --git a/src/types/savedQueryRowProps.ts b/src/types/savedQueryRowProps.ts new file mode 100644 index 0000000..1952d9a --- /dev/null +++ b/src/types/savedQueryRowProps.ts @@ -0,0 +1,8 @@ +import type React from 'react'; +import type {QueryListRowRenderData} from './queryList'; +import type {SavedQuery} from './savedQueries'; + +export type SavedQueryRowProps = { + item: T; + renderAuthor?: (item: T) => React.ReactNode; +} & Omit, 'item' | 'variant'>; diff --git a/src/types/savedQuerySearchRowProps.ts b/src/types/savedQuerySearchRowProps.ts new file mode 100644 index 0000000..83c70f9 --- /dev/null +++ b/src/types/savedQuerySearchRowProps.ts @@ -0,0 +1,8 @@ +import type React from 'react'; +import type {QueryListRowRenderData} from './queryList'; +import type {SavedQuery} from './savedQueries'; + +export type SavedQuerySearchRowProps = { + item: T; + renderAuthor?: (item: T) => React.ReactNode; +} & Omit, 'item' | 'variant'>; diff --git a/src/types/searchRowLayoutProps.ts b/src/types/searchRowLayoutProps.ts new file mode 100644 index 0000000..30f8f2d --- /dev/null +++ b/src/types/searchRowLayoutProps.ts @@ -0,0 +1,12 @@ +import type {ReactNode} from 'react'; +import type {QueryListLinkRenderer} from './queryList'; + +export type SearchRowLayoutProps = { + header: ReactNode; + query?: string; + language: string; + href?: string; + disabled?: boolean; + renderLink?: QueryListLinkRenderer; + className?: string; +}; diff --git a/src/types/searchWithButtonsProps.ts b/src/types/searchWithButtonsProps.ts new file mode 100644 index 0000000..34e3c59 --- /dev/null +++ b/src/types/searchWithButtonsProps.ts @@ -0,0 +1,13 @@ +import type {FlexProps} from '@gravity-ui/uikit'; +import type React from 'react'; + +export type SearchWithButtonsProps = { + value?: string; + placeholder?: string; + hasClear?: boolean; + innerButtons?: React.ReactNode[]; + endButtons?: React.ReactNode[]; + onUpdate?: (value: string) => void; + className?: string; + gap?: FlexProps['gap']; +}; diff --git a/src/types/simpleFormProps.ts b/src/types/simpleFormProps.ts new file mode 100644 index 0000000..565f214 --- /dev/null +++ b/src/types/simpleFormProps.ts @@ -0,0 +1,43 @@ +import type {RangeValue} from '@gravity-ui/date-components'; +import type {DateTime} from '@gravity-ui/date-utils'; +import type {SelectProps} from '@gravity-ui/uikit'; +import type {RegisteredFormFields} from './forms'; + +export type FormField = RegisteredFormFields; + +export type FormValues = Record; +export type SimpleFormProps = { + fields: FormField[]; + values?: FormValues; + initialValues?: FormValues; + onValuesChange?: (values: FormValues) => void; +}; + +export type SwitchFieldProps = { + value: boolean; + title?: string; + onChange: (value: boolean) => void; +}; + +export type RangeDatePickerFieldProps = { + value: RangeValue; + title?: string; + onChange: (value: RangeValue) => void; +}; + +export type SelectFieldProps = { + value: SelectProps['value']; + options: SelectProps['options']; + title?: string; + onChange: (value: string[]) => void; +}; + +export type CheckboxGroupFieldProps = { + value: string[]; + items: CheckboxGroupItem[]; + title?: string; + onChange: (value: string[]) => void; +}; + +export type DateValue = DateTime | null; +export type CheckboxGroupItem = {id: string; title?: string}; diff --git a/src/types/skeletonRowsProps.ts b/src/types/skeletonRowsProps.ts new file mode 100644 index 0000000..ebd184c --- /dev/null +++ b/src/types/skeletonRowsProps.ts @@ -0,0 +1,5 @@ +export type SkeletonRowsProps = { + count?: number; + className?: string; + rowClassName?: string; +}; diff --git a/src/types/tutorialRowProps.ts b/src/types/tutorialRowProps.ts new file mode 100644 index 0000000..74b9ddb --- /dev/null +++ b/src/types/tutorialRowProps.ts @@ -0,0 +1,7 @@ +import type {QueryListLinkRenderer} from './queryList'; +import type {TutorialHistoryRow} from './tutorial'; + +export type TutorialRowProps = { + item: T; + renderLink?: QueryListLinkRenderer; +}; diff --git a/src/types/tutorialSearchRowProps.ts b/src/types/tutorialSearchRowProps.ts new file mode 100644 index 0000000..a70f7cd --- /dev/null +++ b/src/types/tutorialSearchRowProps.ts @@ -0,0 +1,7 @@ +import type {QueryListLinkRenderer} from './queryList'; +import type {TutorialHistoryRow} from './tutorial'; + +export type TutorialSearchRowProps = { + item: T; + renderLink?: QueryListLinkRenderer; +}; From eb503df109f9166bad15946f61c90753fb96fca3 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Wed, 14 Oct 2026 17:22:30 +0300 Subject: [PATCH 02/10] fix(composition): preserve filter drafts and dynamic selection --- .../Dashboard/Dashboard.stories.tsx | 25 +++ src/components/Dashboard/Dashboard.tsx | 10 + .../HistoryFilter/HistoryFilter.stories.tsx | 27 +++ .../HistoryFilter/HistoryFilter.tsx | 33 ++-- .../helpers/equalFilterValues.ts | 22 +++ src/modules/Attachments/Attachments.tsx | 83 ++------- .../NavigationDetail.stories.tsx | 61 +++++++ .../NavigationDetail/NavigationDetail.tsx | 9 +- .../NavigationDetail/helpers/getInitialTab.ts | 4 +- .../components/AttachmentsLifecycle.test.tsx | 97 ++++++++++ .../components/AuditStateRegressions.test.tsx | 172 ++++++++++++++++++ 11 files changed, 461 insertions(+), 82 deletions(-) create mode 100644 src/components/HistoryFilter/helpers/equalFilterValues.ts create mode 100644 src/modules/NavigationDetail/NavigationDetail.stories.tsx create mode 100644 tests/components/AttachmentsLifecycle.test.tsx create mode 100644 tests/components/AuditStateRegressions.test.tsx diff --git a/src/components/Dashboard/Dashboard.stories.tsx b/src/components/Dashboard/Dashboard.stories.tsx index b6640d4..92f417f 100644 --- a/src/components/Dashboard/Dashboard.stories.tsx +++ b/src/components/Dashboard/Dashboard.stories.tsx @@ -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 4a4dfde..573a976 100644 --- a/src/components/Dashboard/Dashboard.tsx +++ b/src/components/Dashboard/Dashboard.tsx @@ -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/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 e2c0b65..af5def3 100644 --- a/src/components/HistoryFilter/HistoryFilter.tsx +++ b/src/components/HistoryFilter/HistoryFilter.tsx @@ -1,4 +1,4 @@ -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'; @@ -6,6 +6,8 @@ import {useToggle} from '../../helpers/useToggle'; 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'; @@ -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 = ({ placement="bottom-start" >
- { - 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/modules/Attachments/Attachments.tsx b/src/modules/Attachments/Attachments.tsx index aad33c6..861f230 100644 --- a/src/modules/Attachments/Attachments.tsx +++ b/src/modules/Attachments/Attachments.tsx @@ -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/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 9ae1e68..ff7979a 100644 --- a/src/modules/NavigationDetail/NavigationDetail.tsx +++ b/src/modules/NavigationDetail/NavigationDetail.tsx @@ -33,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; @@ -70,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/tests/components/AttachmentsLifecycle.test.tsx b/tests/components/AttachmentsLifecycle.test.tsx new file mode 100644 index 0000000..5b25321 --- /dev/null +++ b/tests/components/AttachmentsLifecycle.test.tsx @@ -0,0 +1,97 @@ +// @vitest-environment jsdom +import React, {act} from 'react'; +import {createRoot, type Root} from 'react-dom/client'; +import {afterEach, beforeEach, expect, it, vi} from 'vitest'; +import {Attachments} from '../../src/modules/Attachments'; + +const captures = vi.hoisted(() => ({edit: {} as any, list: {} as any})); +vi.mock('@gravity-ui/uikit', () => { + const Container = ({children}: any) =>
{children}
; + return { + Box: Container, + Flex: Container, + Text: Container, + TabProvider: Container, + TabList: Container, + Tab: Container, + TabPanel: ({children, value}: any) => (value === 'Current' ? children : null), + Icon: () => null, + Button: ({children, onClick}: any) => , + }; +}); +vi.mock('../../src/components/AttachmentList', () => ({ + AttachmentList: (props: any) => { + captures.list = props; + return props.attachments.map((attachment: any) => + props.editingIds.includes(attachment.id) ? ( + + {props.renderEditForm(attachment)} + + ) : ( +
{attachment.name}
+ ), + ); + }, + EditAttachmentItem: (props: any) => { + captures.edit = props; + return null; + }, +})); +vi.mock('../../src/components/AttachmentListPlaceholder', () => ({ + AttachmentListPlaceholder: ({onAddFile, onAddLink}: any) => ( + <> + + + + ), +})); + +let container: HTMLDivElement; +let root: Root; +beforeEach(() => { + globalThis.IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement('div'); + root = createRoot(container); +}); +afterEach(() => act(() => root.unmount())); + +it.each(['file', 'link'] as const)('adds and accepts a %s with one callback per action', (kind) => { + const onChange = vi.fn(); + act(() => root.render()); + act(() => container.querySelectorAll('button')[kind === 'file' ? 0 : 1].click()); + expect(onChange).toHaveBeenCalledOnce(); + const draft = onChange.mock.calls[0][0].attachments[0]; + expect(draft).toMatchObject({name: '', isNew: true}); + expect(captures.edit.type).toBe(kind); + const accepted = + kind === 'file' + ? {name: 'query.sql'} + : {name: 'Docs', link: 'https://example.com', token: 'token'}; + act(() => captures.edit.onAccept(kind === 'file' ? accepted.name : accepted)); + expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange.mock.calls[1][0]).toEqual({ + attachments: [{id: draft.id, ...accepted}], + deletedAttachments: [], + }); + expect(captures.list.editingIds).toEqual([]); + expect(captures.list.wasAddedIds).toEqual([draft.id]); + expect(container.textContent).toContain(accepted.name); +}); + +it('accepts an existing attachment edit and preserves the deleted list', () => { + const onChange = vi.fn(); + const item = {id: 'a', name: 'before.sql'}; + const deleted = [{id: 'old', name: 'old.sql'}]; + act(() => + root.render( + , + ), + ); + act(() => captures.list.onEdit(item)); + act(() => captures.edit.onAccept('after.sql')); + expect(onChange).toHaveBeenCalledExactlyOnceWith({ + attachments: [{id: 'a', name: 'after.sql'}], + deletedAttachments: deleted, + }); + expect(captures.list.wasEditedIds).toEqual(['a']); +}); diff --git a/tests/components/AuditStateRegressions.test.tsx b/tests/components/AuditStateRegressions.test.tsx new file mode 100644 index 0000000..de0210c --- /dev/null +++ b/tests/components/AuditStateRegressions.test.tsx @@ -0,0 +1,172 @@ +// @vitest-environment jsdom +import React, {act} from 'react'; +import {createRoot, type Root} from 'react-dom/client'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {HistoryFilter} from '../../src/components/HistoryFilter'; +import {NavigationDetail} from '../../src/modules/NavigationDetail'; +import {Dashboard} from '../../src/components/Dashboard'; +import type {QueryListFilterConfig} from '../../src/types/queryList'; +import type {NavigationDetailConfig} from '../../src/types/navigation'; + +const captures = vi.hoisted(() => ({form: {} as any, dash: {} as any})); +vi.mock('@gravity-ui/uikit', () => ({ + Button: ({children, onClick}: any) => , + Flex: ({children}: any) =>
{children}
, + Icon: () => null, + Popup: ({children, open}: any) => (open ?
{children}
: null), +})); +vi.mock('../../src/components/SimpleForm', () => ({ + SimpleForm: (props: any) => { + captures.form = props; + return
{JSON.stringify(props.values)}
; + }, +})); +vi.mock('../../src/modules/NavigationHeader', () => ({NavigationHeader: () => null})); +vi.mock('../../src/components/SearchWithButtons', () => ({SearchWithButtons: () => null})); +vi.mock('../../src/modules/NavigationDetail/internal/NavigationDetailTabs', () => ({ + NavigationDetailTabs: ({tabs, onUpdate}: any) => + tabs.map((tab: any) => ( + + )), +})); +vi.mock('../../src/components/Dashboard/internal/DashPlate', () => ({ + DashPlate: () => null, + DRAG_HANDLE_CLASSNAME: 'drag', +})); +vi.mock('@gravity-ui/dashkit', () => ({ + DashKit: Object.assign( + (props: any) => { + captures.dash = props; + return null; + }, + {reloadPlugins: () => {}}, + ), +})); + +let container: HTMLDivElement; +let root: Root; +beforeEach(() => { + globalThis.IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement('div'); + root = createRoot(container); +}); +afterEach(() => act(() => root.unmount())); +const render = (element: React.ReactNode) => act(() => root.render(element)); +function click(text: string) { + const button = [...container.querySelectorAll('button')].find( + (item) => item.textContent === text, + ); + if (!button) throw new Error(`Missing button ${text}`); + act(() => button.click()); +} + +describe('HistoryFilter draft', () => { + const fields: QueryListFilterConfig['fields'] = [ + {id: 'mine', type: 'switch', initialValue: true}, + {id: 'other', type: 'switch', initialValue: false}, + ]; + it('applies displayed defaults, accumulates edits, and resets without remounting', () => { + const onApply = vi.fn(); + const onReset = vi.fn(); + render( + , + ); + click(''); + act(() => container.querySelectorAll('button')[1].click()); + expect(onApply).toHaveBeenLastCalledWith({mine: true, other: true}); + act(() => captures.form.onValuesChange({...captures.form.values, mine: false})); + act(() => captures.form.onValuesChange({...captures.form.values, other: false})); + act(() => container.querySelectorAll('button')[1].click()); + expect(onApply).toHaveBeenLastCalledWith({mine: false, other: false}); + click(''); // closing and reopening preserves the draft + click(''); + expect(captures.form.values).toEqual({mine: false, other: false}); + act(() => container.querySelectorAll('button')[2].click()); + expect(captures.form.values).toEqual({mine: true, other: true}); + expect(onReset).toHaveBeenCalledOnce(); + }); + it('preserves edits for equivalent external objects but accepts changed values', () => { + const show = (values: Record) => + render(); + show({mine: false, selected: ['a'], date: new Date(0)}); + click(''); + act(() => captures.form.onValuesChange({...captures.form.values, other: true})); + show({date: new Date(0), selected: ['a'], mine: false}); + expect(captures.form.values.other).toBe(true); + show({mine: true}); + expect(captures.form.values).toEqual({mine: true, other: false}); + }); +}); + +describe('NavigationDetail dynamic tabs', () => { + const tabs = [ + {id: 'a', title: 'A', content: 'Content A'}, + {id: 'b', title: 'B', content: 'Content B'}, + ]; + it('recovers from loading, hidden, disabled, and removed tabs without emitting selection', () => { + const onTabUpdate = vi.fn(); + const show = (config: NavigationDetailConfig) => + render( + {}} + onTabUpdate={onTabUpdate} + />, + ); + show({tabs: [], emptyContent: 'Empty'}); + expect(container.textContent).toBe('Empty'); + show({tabs, defaultTab: 'b'}); + expect(container.textContent).toContain('Content B'); + show({tabs: [tabs[0], {...tabs[1], hidden: true}], defaultTab: 'b'}); + expect(container.textContent).toContain('Content A'); + show({tabs: [{...tabs[0], disabled: true}, tabs[1]], defaultTab: 'a'}); + expect(container.textContent).toContain('Content B'); + show({tabs: [{...tabs[0], disabled: true}], emptyContent: 'Empty'}); + expect(container.textContent).toBe('Empty'); + expect(onTabUpdate).not.toHaveBeenCalled(); + }); + it('keeps a valid user choice and leaves controlled selection to the caller', () => { + const show = (activeTab?: string) => + render( + {}} + activeTab={activeTab} + />, + ); + show(); + click('A'); + show(); + expect(container.textContent).toContain('Content A'); + show('missing'); + expect(container.textContent).not.toContain('Content A'); + expect(container.textContent).not.toContain('Content B'); + }); +}); + +it('Dashboard forwards current content/index and ignores removed items', () => { + const onItemFocus = vi.fn(); + const onItemBlur = vi.fn(); + const first = {id: 'a', content: 'A'}; + const second = {id: 'b', content: 'B'}; + const show = (items: (typeof first)[]) => + render(); + show([first, second]); + captures.dash.onItemFocus({id: 'b'}); + expect(onItemFocus).toHaveBeenLastCalledWith('B', 1); + show([second, first]); + captures.dash.onItemBlur({id: 'b'}); + expect(onItemBlur).toHaveBeenLastCalledWith('B', 0); + show([first]); + captures.dash.onItemFocus({id: 'b'}); + expect(onItemFocus).toHaveBeenCalledOnce(); +}); From b67392c3fadafef79a4d8247d01faf355e7324c2 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Wed, 14 Oct 2026 17:22:47 +0300 Subject: [PATCH 03/10] fix(editor)!: align engine presets and wire QueryPage integration --- .../clickhouse/clickhouse.ts | 2 + .../QueryPage/story/QueryPage.stories.tsx | 80 +++++++++-- src/widgets/QueryEditor/README.md | 5 +- .../QueryEditor/presets/ytsaurus/README.md | 4 + .../ytsaurus/createYtsaurusQueryEngines.tsx | 4 +- tests/browser/fixture/query-page.tsx | 105 ++++++++++++-- tests/browser/queryPage.spec.ts | 74 ++++++++++ .../QueryPageComparisonStory.test.tsx | 2 + .../components/YtsaurusQueryEngines.test.tsx | 128 +++++++++++++++++- 9 files changed, 376 insertions(+), 28 deletions(-) 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/pages/QueryPage/story/QueryPage.stories.tsx b/src/pages/QueryPage/story/QueryPage.stories.tsx index 7d63913..a766f53 100644 --- a/src/pages/QueryPage/story/QueryPage.stories.tsx +++ b/src/pages/QueryPage/story/QueryPage.stories.tsx @@ -7,16 +7,41 @@ import type {QueryPageComparisonTab, QueryPageQueryTab} from '../../../types/que import type {QueryHistoryRow} from '../../../types/history'; import {BASE_ITEMS} from '../../../modules/QueriesHistory/QueriesHistory.stories.data'; import {useQueriesSidebarTabs} from '../../../widgets/QueriesSidebar/QueriesSidebar.stories.data'; -import { - CLUSTERS, - PARAMETERS, - QUERY, - SETTINGS, - TAB_ENGINES, -} from '../../../widgets/QueryEditor/story/demoData'; +import {CLUSTERS, PARAMETERS, QUERY, SETTINGS} from '../../../widgets/QueryEditor/story/demoData'; +import {createYtsaurusQueryEngines} from '../../../widgets/QueryEditor/presets/ytsaurus'; +import {createQueryEditorPreset} from '../../../helpers/queryEditorPreset'; import {designTabs} from '../../../widgets/QueryExecutionPanel/story/designReference'; import './QueryPage.stories.scss'; +const engines = createYtsaurusQueryEngines({ + yql: {versions: ['stable', 'preview']}, + chyt: {cliques: [{alias: 'analytics', state: 'active', health: 'good'}]}, + spyt: {discoveryGroups: [{path: '//sys/spark/clusters/demo', title: 'Demo Spark'}]}, +}); +const preset = createQueryEditorPreset({ + listPathChildren: ({language, token}) => + token.isCancellationRequested + ? [] + : [ + { + name: 'events', + path: + language === 'spyt' || language === 'clickhouse' + ? 'events' + : '//demo/events', + kind: 'table', + }, + ], + getTableSchema: ({token}) => + token.isCancellationRequested + ? [] + : [ + {name: 'region', type: 'String'}, + {name: 'event_id', type: 'Uint64'}, + ], + onReferenceClick: action('navigateToTable'), +}); + function Notes() { const [value, setValue] = useState(''); return ( @@ -34,6 +59,9 @@ type ExampleProps = { resultsCollapsed?: boolean; resultsExpanded?: boolean; controlled?: boolean; + fourEngines?: boolean; + initialComparison?: boolean; + initialActiveTab?: string; }; function Example({ @@ -41,6 +69,9 @@ function Example({ resultsCollapsed = false, resultsExpanded = false, controlled = false, + fourEngines = false, + initialComparison = false, + initialActiveTab = '1', }: ExampleProps) { const createDraft = (id: string) => ({ id, @@ -49,16 +80,20 @@ function Example({ savedValue: id === '2' ? 'SELECT region, COUNT(*) FROM `//demo/events` GROUP BY region;\n' : QUERY, clusterId: CLUSTERS[id === '2' ? 1 : 0].id, - engineId: TAB_ENGINES[0].id, + engineId: fourEngines ? engines[(Number(id) - 1) % engines.length].id : engines[0].id, parameters: PARAMETERS.map((parameter) => ({...parameter})), settings: {...SETTINGS, queryName: `Query ${id}`}, executionPanelHeight: 300, collapsed: resultsCollapsed, expanded: resultsExpanded, }); - const [drafts, setDrafts] = useState(() => [createDraft('1'), createDraft('2')]); - const [activeId, setActiveId] = useState('1'); - const nextId = useRef(3); + const [drafts, setDrafts] = useState(() => + (fourEngines ? ['1', '2', '3', '4'] : ['1', '2']).map(createDraft), + ); + const [activeId, setActiveId] = useState( + initialComparison ? 'comparison-initial' : initialActiveTab, + ); + const nextId = useRef(fourEngines ? 5 : 3); const [comparisons, setComparisons] = useState< { id: string; @@ -67,7 +102,19 @@ function Example({ value: string | null; savedValue: string | null; }[] - >([]); + >(() => { + if (!initialComparison) return []; + const rows = BASE_ITEMS.filter((item): item is QueryHistoryRow => 'id' in item); + return [ + { + id: 'comparison-initial', + original: rows[0], + modified: rows[1], + value: rows[1].query ?? null, + savedValue: rows[1].query ?? null, + }, + ]; + }); const [historyItems, setHistoryItems] = useState(BASE_ITEMS); const historyRows = historyItems.filter((item): item is QueryHistoryRow => 'id' in item); const versionId = (row: QueryHistoryRow) => `${typeof row.id}:${row.id}`; @@ -273,7 +320,8 @@ function Example({ clusterId: draft.clusterId, onClusterChange: (clusterId) => updateDraft(draft.id, {clusterId}), - engines: TAB_ENGINES, + engines, + preset, engineId: draft.engineId, onEngineChange: (engineId) => updateDraft(draft.id, {engineId}), @@ -361,15 +409,17 @@ type Story = StoryObj; export const Default: Story = {}; export const HistoryComparison: Story = { + args: {initialComparison: true}, parameters: { docs: { description: { - story: 'Open a history row menu, choose Select for Compare, select a second row, then compare. Rename and Delete update the demo history. Comparison tabs support editing the right-hand draft, reset/save, creating a query from the current draft, version selectors, and split/unified modes.', + story: 'Starts with an editable comparison. To open another, use Select for Compare in the history row menu. Rename and Delete update the demo history. Comparison tabs support editing the right-hand draft, reset/save, creating a query from the current draft, version selectors, and split/unified modes.', }, }, }, }; export const IndependentTabs: Story = { + args: {initialActiveTab: '2'}, parameters: { docs: { description: { @@ -382,3 +432,5 @@ export const HiddenSidebar: Story = {args: {sidebarHidden: true}}; export const CollapsedResults: Story = {args: {resultsCollapsed: true}}; export const ExpandedResults: Story = {args: {resultsExpanded: true}}; export const ControlledLayout: Story = {args: {controlled: true}}; + +export const FourEngines: Story = {args: {fourEngines: true}}; diff --git a/src/widgets/QueryEditor/README.md b/src/widgets/QueryEditor/README.md index bec7b7e..ccad1c0 100644 --- a/src/widgets/QueryEditor/README.md +++ b/src/widgets/QueryEditor/README.md @@ -204,7 +204,6 @@ in a request. For custom content, use `parameters={}` instead; `parameters` and `parametersProps` are mutually exclusive, just like settings. - ## Storybook examples `Default` combines editable settings, parameters, attachments, and sharing using local @@ -223,3 +222,7 @@ Storybook toolbar. The `Extensions` group contains the specialized Monaco exampl Use `@gravity-ui/querieskit/widgets/QueryEditor` and its `presets/ytsaurus` subpath. The previous `modules/QueryEditor` and `modules/QueryEditor/presets/ytsaurus` package subpaths remain aliases of these widget entrypoints. Root imports are unchanged. + +### Monaco language services + +The engine factory maps YQL to `yql`, CHYT to `clickhouse`, SPYT to `spyt`, and YT QL to `ytql`. Pass `preset={createQueryEditorPreset(...)}` alongside `engines` to enable completion and reference navigation for all four. The factory alone configures the engine selector and settings, not language providers. Older SPYT/QL descriptors used `s-expression`/`yql_ansi`; update custom provider language checks when migrating. diff --git a/src/widgets/QueryEditor/presets/ytsaurus/README.md b/src/widgets/QueryEditor/presets/ytsaurus/README.md index 754dd73..3e525df 100644 --- a/src/widgets/QueryEditor/presets/ytsaurus/README.md +++ b/src/widgets/QueryEditor/presets/ytsaurus/README.md @@ -53,3 +53,7 @@ text to their backend format when submitting the query. For JSON language services, route the `json` label in your Monaco environment worker configuration to `monaco-editor/languages/features/json/json.worker`. Use the worker setup appropriate for your bundler, alongside the editor worker. + +### Monaco language services + +The engine factory maps YQL to `yql`, CHYT to `clickhouse`, SPYT to `spyt`, and YT QL to `ytql`. Pass `preset={createQueryEditorPreset(...)}` alongside `engines` to enable completion and reference navigation for all four. The factory alone configures the engine selector and settings, not language providers. Older SPYT/QL descriptors used `s-expression`/`yql_ansi`; update custom provider language checks when migrating. diff --git a/src/widgets/QueryEditor/presets/ytsaurus/createYtsaurusQueryEngines.tsx b/src/widgets/QueryEditor/presets/ytsaurus/createYtsaurusQueryEngines.tsx index 604a802..9001bd8 100644 --- a/src/widgets/QueryEditor/presets/ytsaurus/createYtsaurusQueryEngines.tsx +++ b/src/widgets/QueryEditor/presets/ytsaurus/createYtsaurusQueryEngines.tsx @@ -77,7 +77,7 @@ export function createYtsaurusQueryEngines< spyt: { id: 'spyt', title: 'SPYT', - language: 's-expression', + language: 'spyt', renderSettingsSections: (context) => options.spyt?.connectAvailable ? : null, renderSettingsFields: (context) => @@ -97,7 +97,7 @@ export function createYtsaurusQueryEngines< /> ), }, - ql: {id: 'ql', title: 'YT QL', language: 'yql_ansi'}, + ql: {id: 'ql', title: 'YT QL', language: 'ytql'}, }; return (options.engines ?? defaultEngines).map((engine) => engines[engine]); diff --git a/tests/browser/fixture/query-page.tsx b/tests/browser/fixture/query-page.tsx index be94f96..91049e0 100644 --- a/tests/browser/fixture/query-page.tsx +++ b/tests/browser/fixture/query-page.tsx @@ -1,7 +1,12 @@ -import React, {useState} from 'react'; +import React, {createRef, useState} from 'react'; import {createRoot} from 'react-dom/client'; import {TextInput, ThemeProvider, configure} from '@gravity-ui/uikit'; import EditorWorker from 'monaco-editor/editor/editor.worker?worker'; +import * as monaco from 'monaco-editor/editor/editor.api'; +import {createYtsaurusQueryEngines} from '../../../src/widgets/QueryEditor/presets/ytsaurus'; +import {createQueryEditorPreset} from '../../../src/helpers/queryEditorPreset'; +import type {EditorPreset} from '../../../src/types/editorProviders'; +import type {YtsaurusQueryEngineId} from '../../../src/types/ytsaurusQueryEngines'; import {QueryPage} from '../../../src/pages/QueryPage'; import {setLang} from '../../../src/i18n'; import '@gravity-ui/uikit/styles/styles.css'; @@ -9,14 +14,79 @@ import '@gravity-ui/uikit/styles/styles.css'; self.MonacoEnvironment = {getWorker: () => new EditorWorker()}; configure({lang: 'en'}); setLang('en'); +const engines = createYtsaurusQueryEngines(); +const refs: Record> = { + '1': createRef(), + '2': createRef(), +}; +const requests: {engineId?: string; language?: string; clusterId?: string}[] = []; +const lifecycle = {created: 0, disposed: 0}; +const basePreset = createQueryEditorPreset({ + listPathChildren(request) { + requests.push({ + engineId: request.engineId, + language: request.language, + clusterId: request.clusterId, + }); + return [{name: `catalog_${request.clusterId}_${request.engineId}`, kind: 'table'}]; + }, +}); +const preset: EditorPreset = { + create(context) { + lifecycle.created++; + const instance = basePreset.create(context); + return { + ...instance, + dispose() { + lifecycle.disposed++; + instance.dispose?.(); + }, + }; + }, +}; +const queryPageTest = { + snapshot() { + return { + models: monaco.editor + .getModels() + .map((model) => ({uri: model.uri.toString(), language: model.getLanguageId()})), + editors: Object.fromEntries( + Object.entries(refs).map(([id, ref]) => [ + id, + { + language: ref.current?.getModel()?.getLanguageId(), + uri: ref.current?.getModel()?.uri.toString(), + }, + ]), + ), + requests: [...requests], + lifecycle: {...lifecycle}, + }; + }, + suggest(id: string) { + const editor = refs[id]?.current; + if (!editor) throw new Error(`Missing editor ${id}`); + editor.setValue('SELECT * FROM catalog_'); + editor.setPosition({lineNumber: 1, column: 23}); + editor.focus(); + editor.trigger('test', 'editor.action.triggerSuggest', {}); + }, +}; +declare global { + interface Window { + queryPageTest: typeof queryPageTest; + } +} +window.queryPageTest = queryPageTest; + function Draft({label}: {label: string}) { const [value, setValue] = useState(''); return ; } function Example() { const [queries, setQueries] = useState([ - {id: '1', value: 'SELECT 1;'}, - {id: '2', value: 'SELECT 2;'}, + {id: '1', value: 'SELECT 1;', engineId: 'yql' as YtsaurusQueryEngineId, clusterId: 'c'}, + {id: '2', value: 'SELECT 2;', engineId: 'yql' as YtsaurusQueryEngineId, clusterId: 'c'}, ]); const [activeTab, setActiveTab] = useState('1'); return ( @@ -57,12 +127,29 @@ function Example() { item.id === query.id ? {...item, value} : item, ), ), - clusters: [{id: 'c', title: 'Cluster'}], - clusterId: 'c', - onClusterChange() {}, - engines: [{id: 'e', title: 'SQL', language: 'sql'}], - engineId: 'e', - onEngineChange() {}, + clusters: [ + {id: 'c', title: 'Cluster'}, + {id: 'secondary', title: 'Secondary'}, + ], + clusterId: query.clusterId, + onClusterChange: (clusterId) => + setQueries((items) => + items.map((item) => + item.id === query.id ? {...item, clusterId} : item, + ), + ), + engines, + engineId: query.engineId, + onEngineChange: (engineId) => + setQueries((items) => + items.map((item) => + item.id === query.id + ? {...item, engineId: engineId as YtsaurusQueryEngineId} + : item, + ), + ), + preset, + editorRef: refs[query.id], onRun() {}, onFormat() {}, onValidate() {}, diff --git a/tests/browser/queryPage.spec.ts b/tests/browser/queryPage.spec.ts index 3dd253c..4bc0fc7 100644 --- a/tests/browser/queryPage.spec.ts +++ b/tests/browser/queryPage.spec.ts @@ -1,4 +1,5 @@ import {expect, test} from '@playwright/test'; +import type {} from './fixture/query-page'; test.beforeEach(async ({page}) => { await page.goto('/query-page.html'); @@ -186,3 +187,76 @@ test('tab header toggle hides the entire sidebar and restores its width', async 'retained', ); }); + +test('factory engines activate their presets in each QueryPage editor and release them on close', async ({ + page, +}) => { + const first = page.getByRole('tabpanel', {name: 'Query 1', exact: true, includeHidden: true}); + const pageErrors: string[] = []; + page.on('pageerror', (error) => pageErrors.push(error.message)); + const initial = await page.evaluate(() => window.queryPageTest.snapshot()); + await first.getByRole('combobox', {name: 'Cluster', exact: true}).click(); + await page.getByRole('option', {name: 'Secondary', exact: true}).click(); + for (const [engineId, title, language] of [ + ['yql', 'YQL', 'yql'], + ['chyt', 'CHYT', 'clickhouse'], + ['spyt', 'SPYT', 'spyt'], + ['ql', 'YT QL', 'ytql'], + ]) { + if (engineId !== 'yql') { + await first.getByRole('combobox', {name: 'Engine', exact: true}).click(); + await page.getByRole('option', {name: title, exact: true}).click(); + } + await expect + .poll( + async () => + (await page.evaluate(() => window.queryPageTest.snapshot())).editors['1'] + .language, + ) + .toBe(language); + await page.evaluate(() => window.queryPageTest.suggest('1')); + await expect( + first + .locator('.suggest-widget .monaco-list-row') + .filter({hasText: `catalog_secondary_${engineId}`}), + ).toBeVisible(); + await expect + .poll( + async () => + (await page.evaluate(() => window.queryPageTest.snapshot())).requests.slice( + -1, + )[0], + ) + .toEqual({engineId, language, clusterId: 'secondary'}); + await page.keyboard.press('Escape'); + const current = await page.evaluate(() => window.queryPageTest.snapshot()); + expect(current.editors['1'].uri).toBe(initial.editors['1'].uri); + expect(current.models).toHaveLength(initial.models.length); + expect(current.lifecycle.created - current.lifecycle.disposed).toBe(1); + } + await page.getByRole('tab', {name: 'Query 2', exact: true}).click(); + await expect + .poll( + async () => + (await page.evaluate(() => window.queryPageTest.snapshot())).editors['2'].language, + ) + .toBe('yql'); + await page.evaluate(() => window.queryPageTest.suggest('2')); + await expect( + page + .getByRole('tabpanel', {name: 'Query 2', exact: true}) + .locator('.suggest-widget .monaco-list-row') + .filter({hasText: 'catalog_c_yql'}), + ).toBeVisible(); + await page.keyboard.press('Escape'); + await page.getByRole('button', {name: 'Close tab Query 1', exact: true}).click(); + await expect + .poll( + async () => (await page.evaluate(() => window.queryPageTest.snapshot())).models.length, + ) + .toBe(initial.models.length); + const closed = await page.evaluate(() => window.queryPageTest.snapshot()); + expect(closed.models.some(({uri}) => uri === initial.editors['1'].uri)).toBe(false); + expect(closed.lifecycle.created - closed.lifecycle.disposed).toBe(1); + expect(pageErrors).toEqual([]); +}); diff --git a/tests/components/QueryPageComparisonStory.test.tsx b/tests/components/QueryPageComparisonStory.test.tsx index 20a7bfe..5e42865 100644 --- a/tests/components/QueryPageComparisonStory.test.tsx +++ b/tests/components/QueryPageComparisonStory.test.tsx @@ -20,6 +20,8 @@ vi.mock('../../src/widgets/QueriesSidebar/QueriesSidebar.stories.data', () => ({ ], })); vi.mock('../../src/widgets/QueryExecutionPanel/story/designReference', () => ({designTabs: []})); +// The story configures real engine descriptors; Monaco itself is exercised in browser tests. +vi.mock('../../src/components/MonacoEditor', () => ({MonacoEditor: () => null})); vi.mock('storybook/actions', () => ({action: () => vi.fn()})); const Example = meta.component; diff --git a/tests/components/YtsaurusQueryEngines.test.tsx b/tests/components/YtsaurusQueryEngines.test.tsx index 2761b58..6975416 100644 --- a/tests/components/YtsaurusQueryEngines.test.tsx +++ b/tests/components/YtsaurusQueryEngines.test.tsx @@ -11,6 +11,10 @@ import type { YtsaurusQueryEnginesOptions, } from '../../src/types/ytsaurusQueryEngines'; import type {MonacoEditorProps} from '../../src/types/monacoEditor'; +import type * as Monaco from 'monaco-editor'; +import {QueryEditor} from '../../src/widgets/QueryEditor'; +import {createQueryEditorPreset} from '../../src/helpers/queryEditorPreset'; +import type {EditorSetupContext} from '../../src/types/editorProviders'; import {setLang} from '../../src/i18n'; // Exercise the adapter contract without relying on popup positioning in jsdom. @@ -45,6 +49,16 @@ describe('createYtsaurusQueryEngines', () => { beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true; + vi.stubGlobal( + 'matchMedia', + vi.fn(() => ({ + matches: false, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })), + ); configure({lang: 'en'}); setLang('en'); onChange.mockClear(); @@ -78,6 +92,7 @@ describe('createYtsaurusQueryEngines', () => { act(() => root.unmount()); container.remove(); vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); function render(engineId: YtsaurusQueryEngineId, options: YtsaurusQueryEnginesOptions = {}) { @@ -108,8 +123,8 @@ describe('createYtsaurusQueryEngines', () => { expect(createYtsaurusQueryEngines().map(({id, language}) => ({id, language}))).toEqual([ {id: 'yql', language: 'yql'}, {id: 'chyt', language: 'clickhouse'}, - {id: 'spyt', language: 's-expression'}, - {id: 'ql', language: 'yql_ansi'}, + {id: 'spyt', language: 'spyt'}, + {id: 'ql', language: 'ytql'}, ]); expect(createYtsaurusQueryEngines({engines: ['ql', 'chyt']}).map(({id}) => id)).toEqual([ 'ql', @@ -118,6 +133,115 @@ describe('createYtsaurusQueryEngines', () => { expect(createYtsaurusQueryEngines({engines: []})).toEqual([]); }); + it.each(['yql', 'chyt', 'spyt', 'ql'] as const)( + 'connects the %s factory engine through QueryEditor to catalog, references and cleanup', + async (engineId) => { + const listPathChildren = vi.fn(() => [{name: 'events', kind: 'table' as const}]); + const onReferenceClick = vi.fn(); + const preset = createQueryEditorPreset({listPathChildren, onReferenceClick}); + act(() => + root.render( + + + , + ), + ); + const props = editor.props; + if (!props?.preset || !props.providerContext) + throw new Error('Missing preset integration'); + expect(props.preset).toBe(preset); + expect(props.providerContext).toMatchObject({ + engineId, + clusterId: 'primary', + data: {tenant: 'integration'}, + }); + let query = 'SELECT * FROM '; + const model = { + getValue: () => query, + getLanguageId: () => props.language, + getOffsetAt: ({column}: Monaco.Position) => column - 1, + getPositionAt: (offset: number) => ({lineNumber: 1, column: offset + 1}), + } as Monaco.editor.ITextModel; + const mouse = vi.fn(); + const releaseMouse = vi.fn(); + const releaseLanguage = vi.fn(); + const monaco = { + languages: { + CompletionItemKind: { + Keyword: 1, + TypeParameter: 2, + Function: 3, + Variable: 4, + Folder: 5, + Struct: 6, + Field: 7, + }, + CompletionItemInsertTextRule: {InsertAsSnippet: 4}, + getLanguages: () => [], + register: vi.fn(), + setMonarchTokensProvider: () => ({dispose: releaseLanguage}), + setLanguageConfiguration: () => ({dispose: releaseLanguage}), + }, + editor: {MouseTargetType: {CONTENT_TEXT: 6}}, + } as unknown as EditorSetupContext['monaco']; + const instance = props.preset.create({ + model, + monaco, + context: props.providerContext, + editor: { + onMouseDown: mouse.mockReturnValue({dispose: releaseMouse}), + } as unknown as Monaco.editor.IStandaloneCodeEditor, + }); + try { + const completion = instance.providers?.completion; + if (!completion) throw new Error('Missing completion provider'); + const result = await completion.provideCompletionItems( + model, + {lineNumber: 1, column: query.length + 1} as Monaco.Position, + {triggerKind: 0}, + {isCancellationRequested: false} as Monaco.CancellationToken, + ); + expect(result?.suggestions.some(({label}) => label === 'events')).toBe(true); + expect(listPathChildren).toHaveBeenCalledWith( + expect.objectContaining({ + engineId, + language: props.language, + clusterId: 'primary', + data: {tenant: 'integration'}, + }), + ); + query += 'events'; + mouse.mock.calls[0][0]({ + target: {type: 6, position: {lineNumber: 1, column: 16}}, + event: {ctrlKey: true}, + }); + expect(onReferenceClick).toHaveBeenCalledWith( + expect.objectContaining({identifier: 'events', clusterId: 'primary'}), + ); + } finally { + instance.dispose?.(); + } + expect(releaseMouse).toHaveBeenCalledTimes(1); + expect(releaseLanguage).toHaveBeenCalledTimes( + engineId === 'spyt' || engineId === 'ql' ? 2 : 0, + ); + }, + ); + it('hides YQL without versions and renders no engine field for QL or SPYT Connect', () => { expect(render('yql')).toBeUndefined(); expect(render('yql', {yql: {versions: []}})).toBeUndefined(); From 91f0c2f25ff265ae9a436db10e721569e384f535 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Wed, 14 Oct 2026 17:23:06 +0300 Subject: [PATCH 04/10] refactor(renderers): split parameter values and query graph lifecycle --- src/components/QueryGraph/QueryGraph.scss | 89 -- src/components/QueryGraph/QueryGraph.tsx | 445 +------ .../QueryGraph/internal/QueryGraphPopup.scss | 28 + .../QueryGraph/internal/QueryGraphPopup.tsx | 1 + .../internal/QueryGraphRenderer.scss | 50 + .../internal/QueryGraphRenderer.tsx | 300 +++++ .../QueryGraph/internal/QueryGraphState.scss | 14 + .../QueryGraph/internal/QueryGraphState.tsx | 18 + .../internal/QueryGraphWithLayout.tsx | 133 ++ .../internal/_queryGraphLayout.scss | 7 + .../QueryGraph/internal/queryGraphTypes.ts | 1 + src/modules/QueryParameters/i18n/en.json | 6 +- src/modules/QueryParameters/i18n/ru.json | 4 +- .../internal/CollectionValue.scss | 14 + .../internal/CollectionValue.tsx | 124 ++ .../QueryParameters/internal/DictEntry.scss | 6 + .../QueryParameters/internal/DictEntry.tsx | 151 +++ .../internal/NullableValue.scss | 38 + .../internal/NullableValue.tsx | 135 ++ .../internal/ParameterValue.scss | 154 --- .../internal/ParameterValue.tsx | 1155 +---------------- .../internal/ParameterValueContent.scss | 10 + .../internal/ParameterValueContent.tsx | 148 +++ .../internal/ParameterValueHeaders.scss | 20 + .../internal/ParameterValueHeaders.tsx | 114 ++ .../QueryParameters/internal/ScalarValue.scss | 10 + .../QueryParameters/internal/ScalarValue.tsx | 192 +++ .../QueryParameters/internal/StructValue.scss | 7 + .../QueryParameters/internal/StructValue.tsx | 57 + .../internal/ValueActions.scss | 8 + .../QueryParameters/internal/ValueActions.tsx | 52 + .../internal/VariantValue.scss | 9 + .../QueryParameters/internal/VariantValue.tsx | 92 ++ .../internal/_parameterValueLayout.scss | 59 + .../internal/parameterValueHelpers.ts | 61 + .../internal/parameterValueTypes.ts | 26 + .../internal/useChildValidity.ts | 23 + .../internal/useRepresentation.ts | 67 + tests/components/QueryParameters.test.tsx | 29 + 39 files changed, 2025 insertions(+), 1832 deletions(-) create mode 100644 src/components/QueryGraph/internal/QueryGraphPopup.scss create mode 100644 src/components/QueryGraph/internal/QueryGraphRenderer.scss create mode 100644 src/components/QueryGraph/internal/QueryGraphRenderer.tsx create mode 100644 src/components/QueryGraph/internal/QueryGraphState.scss create mode 100644 src/components/QueryGraph/internal/QueryGraphState.tsx create mode 100644 src/components/QueryGraph/internal/QueryGraphWithLayout.tsx create mode 100644 src/components/QueryGraph/internal/_queryGraphLayout.scss create mode 100644 src/components/QueryGraph/internal/queryGraphTypes.ts create mode 100644 src/modules/QueryParameters/internal/CollectionValue.scss create mode 100644 src/modules/QueryParameters/internal/CollectionValue.tsx create mode 100644 src/modules/QueryParameters/internal/DictEntry.scss create mode 100644 src/modules/QueryParameters/internal/DictEntry.tsx create mode 100644 src/modules/QueryParameters/internal/NullableValue.scss create mode 100644 src/modules/QueryParameters/internal/NullableValue.tsx delete mode 100644 src/modules/QueryParameters/internal/ParameterValue.scss create mode 100644 src/modules/QueryParameters/internal/ParameterValueContent.scss create mode 100644 src/modules/QueryParameters/internal/ParameterValueContent.tsx create mode 100644 src/modules/QueryParameters/internal/ParameterValueHeaders.scss create mode 100644 src/modules/QueryParameters/internal/ParameterValueHeaders.tsx create mode 100644 src/modules/QueryParameters/internal/ScalarValue.scss create mode 100644 src/modules/QueryParameters/internal/ScalarValue.tsx create mode 100644 src/modules/QueryParameters/internal/StructValue.scss create mode 100644 src/modules/QueryParameters/internal/StructValue.tsx create mode 100644 src/modules/QueryParameters/internal/ValueActions.scss create mode 100644 src/modules/QueryParameters/internal/ValueActions.tsx create mode 100644 src/modules/QueryParameters/internal/VariantValue.scss create mode 100644 src/modules/QueryParameters/internal/VariantValue.tsx create mode 100644 src/modules/QueryParameters/internal/_parameterValueLayout.scss create mode 100644 src/modules/QueryParameters/internal/parameterValueHelpers.ts create mode 100644 src/modules/QueryParameters/internal/parameterValueTypes.ts create mode 100644 src/modules/QueryParameters/internal/useChildValidity.ts create mode 100644 src/modules/QueryParameters/internal/useRepresentation.ts 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.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/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 ( + +