Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/components/DataTypePopup/DataTypePopup.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.qp-data-type-popup {
min-width: 0;

&_interactive {
cursor: pointer;
}
border-radius: var(--g-border-radius-xs);

&:focus-visible {
outline: 2px solid var(--g-color-line-focus);
outline-offset: 2px;
}

&__icon {
flex-shrink: 0;
color: var(--g-color-text-hint);
}

&__content {
max-height: 300px;
max-width: min(600px, calc(100vw - 32px));
overflow: auto;
padding: var(--g-spacing-3);
}
}
96 changes: 96 additions & 0 deletions src/components/DataTypePopup/DataTypePopup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import type {Meta, StoryObj} from '@storybook/react';
import {ThemeProvider} from '@gravity-ui/uikit';
import {DataTypePopup} from './DataTypePopup';
import type {ParameterType} from '../../types/parameterWizard';

const nestedType: ParameterType = {
name: 'Struct',
struct: [
{key: 'id', type: {name: 'Uint64'}},
{key: 'name', type: {name: 'Utf8', optional: true}},
{
key: 'items',
type: {
name: 'List',
type: {
name: 'Struct',
struct: [
{key: 'price', type: {name: 'Decimal', params: [22, 9]}},
{key: 'tags', type: {name: 'Set', type: {name: 'String'}}},
],
},
},
},
],
};

const meta = {
title: 'Components/DataTypePopup',
component: DataTypePopup,
tags: ['autodocs'],
parameters: {layout: 'centered'},
args: {children: 'Uint64', type: {name: 'Uint64'}},
} satisfies Meta<typeof DataTypePopup>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primitive: Story = {};
export const Nested: Story = {args: {children: 'Struct', type: nestedType}};
export const OptionalAndTagged: Story = {
args: {
children: 'Decimal',
type: {
name: 'Decimal',
params: [22, 9],
optional: true,
optionalLevel: 3,
tagged: true,
tags: ['Money', 'USD'],
},
},
};
export const Dictionary: Story = {
args: {
children: 'Dict',
type: {name: 'Dict', type: [{name: 'Utf8'}, {name: 'List', type: {name: 'Int64'}}]},
},
};
export const Variant: Story = {
args: {
children: 'Variant',
type: {
name: 'Variant',
struct: [
{key: 'text', type: {name: 'Utf8'}},
{key: 'point', type: {name: 'Tuple', type: [{name: 'Double'}, {name: 'Double'}]}},
{key: 'status', type: {name: 'Enum', type: [{name: 'ready'}, {name: 'pending'}]}},
],
},
},
};
export const WithoutIcon: Story = {args: {...Nested.args, hideIcon: true}};
export const WithoutType: Story = {args: {children: 'No type information', type: undefined}};
export const LongStructure: Story = {
args: {
children: 'Struct with 50 fields',
type: {
name: 'Struct',
struct: Array.from({length: 50}, (_, index) => ({
key: `field_${index}`,
type: {name: 'Utf8'},
})),
},
},
};
export const DarkTheme: Story = {
args: Nested.args,
decorators: [
(Story) => (
<ThemeProvider theme="dark">
<Story />
</ThemeProvider>
),
],
};
41 changes: 41 additions & 0 deletions src/components/DataTypePopup/DataTypePopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import {CircleInfo} from '@gravity-ui/icons';
import {Flex, Icon, Popover} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';
import type {DataTypePopupProps} from '../../types/dataTypePopup';
import {formatType} from '../../helpers/dataTypes';
import {DataTypeTree} from './internal/DataTypeTree';
import './DataTypePopup.scss';

const block = cn('qp-data-type-popup');

export function DataTypePopup({children, type, hideIcon, className}: DataTypePopupProps) {
if (!type) {
return <span className={block(null, className)}>{children}</span>;
}
return (
<Popover
hasArrow={false}
enableSafePolygon
content={
<div className={block('content')}>
<DataTypeTree key={formatType(type)} type={type} />
</div>
}
>
<Flex
as="span"
inline
alignItems="center"
gap={1}
className={block({interactive: true}, className)}
role="button"
tabIndex={0}
aria-label={formatType(type)}
>
{children}
{!hideIcon && <Icon className={block('icon')} data={CircleInfo} size={12} />}
</Flex>
</Popover>
);
}
24 changes: 24 additions & 0 deletions src/components/DataTypePopup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# DataTypePopup

Displays a parsed data type in an interactive popover. It uses the same
`ParameterType` descriptor as ParameterWizard and has no dependencies on the YQL
application or its type parser.

```tsx
import {DataTypePopup} from '@gravity-ui/querieskit';

<DataTypePopup type={{name: 'List', type: {name: 'Uint64'}}}>List&lt;Uint64&gt;</DataTypePopup>;
```

- `type`: optional type descriptor. Without it, children render without a popup.
- `children`: visible trigger content.
- `hideIcon`: hides the information icon.
- `className`: styles the trigger.

The popover supports hover, click and keyboard activation. Container types form
an expandable tree; the first two levels start expanded. Struct/Variant fields,
Enum members, Decimal parameters, Optional depth and tags are displayed. Content
scrolls when it exceeds 300px in height.

See **Components / DataTypePopup** in Storybook. ParameterWizard uses this component
for its visible type labels.
4 changes: 4 additions & 0 deletions src/components/DataTypePopup/i18n/dicts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import en from './en.json';
import ru from './ru.json';

export default {en, ru};
4 changes: 4 additions & 0 deletions src/components/DataTypePopup/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"action_expand": "Expand {{type}}",
"action_collapse": "Collapse {{type}}"
}
4 changes: 4 additions & 0 deletions src/components/DataTypePopup/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {addI18Keysets} from '../../../i18n';
import dicts from './dicts';

export default addI18Keysets('qp:data-type-popup', dicts);
4 changes: 4 additions & 0 deletions src/components/DataTypePopup/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"action_expand": "Развернуть {{type}}",
"action_collapse": "Свернуть {{type}}"
}
2 changes: 2 additions & 0 deletions src/components/DataTypePopup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {DataTypePopup} from './DataTypePopup';
export type {DataTypePopupProps} from '../../types/dataTypePopup';
24 changes: 24 additions & 0 deletions src/components/DataTypePopup/internal/DataTypeTree.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.qp-data-type-tree {
min-width: 0;
overflow-wrap: anywhere;

&__toggle {
--g-button-padding: 0;
font-family: var(--g-font-family-monospace);
font-weight: 600;
--g-button-text-color: var(--g-color-text-link);
}

&__optional {
font-size: var(--g-text-code-1-font-size);
font-style: italic;
}

&__children {
padding-inline-start: var(--g-spacing-4);
}

&__expand {
align-self: flex-start;
}
}
109 changes: 109 additions & 0 deletions src/components/DataTypePopup/internal/DataTypeTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, {useState} from 'react';
import {Button, Flex, Text} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';
import type {ParameterType} from '../../../types/parameterWizard';
import {optionalDepth} from '../../../helpers/dataTypes';
import i18n from '../i18n';
import './DataTypeTree.scss';

const block = cn('qp-data-type-tree');

function TypeChildren({type, level}: {type: ParameterType; level: number}) {
if ('struct' in type) {
return (
<>
{type.struct.map((entry, index) => (
<DataTypeTree key={index} type={entry.type} member={entry.key} level={level} />
))}
</>
);
}
if (type.name === 'Enum') {
return (
<>
{type.type.map((entry, index) => (
<Text key={index} variant="code-1">
{entry.name}
</Text>
))}
</>
);
}
if ('type' in type) {
const children = Array.isArray(type.type) ? type.type : [type.type];
return (
<>
{children.map((child, index) => (
<DataTypeTree key={index} type={child} level={level} />
))}
</>
);
}
return null;
}

export function DataTypeTree({
type,
level = 0,
member,
}: {
type: ParameterType;
level?: number;
member?: string | number;
}) {
const [expanded, setExpanded] = useState(level < 2);
const complex = 'struct' in type || 'type' in type;
const depth = optionalDepth(type);
const name = type.name + (type.params?.length ? `(${type.params.join(', ')})` : '');
return (
<Flex className={block()} direction="column" gap={1}>
<Flex alignItems="center" gap={1} wrap="wrap">
{member !== undefined && <Text variant="code-1">{member}:</Text>}
{complex ? (
<Button
className={block('toggle')}
view="flat"
size="xs"
aria-label={i18n(expanded ? 'action_collapse' : 'action_expand', {
type: name,
})}
aria-expanded={expanded}
onClick={() => setExpanded((current) => !current)}
>
{name}
</Button>
) : (
<Text variant="code-1">{name}</Text>
)}
{depth > 0 && (
<Text className={block('optional')} color="secondary">
{depth > 1 ? `Optional × ${depth}` : 'Optional'}
</Text>
)}
{type.tags?.map((tag, index) => (
<Text key={index} variant="code-1">
[{tag}]
</Text>
))}
</Flex>
{complex && (
<Flex className={block('children')} direction="column" gap={1}>
{expanded ? (
<TypeChildren type={type} level={level + 1} />
) : (
<Button
className={block('expand')}
view="flat"
size="xs"
aria-label={i18n('action_expand', {type: name})}
aria-expanded={false}
onClick={() => setExpanded(true)}
>
…
</Button>
)}
</Flex>
)}
</Flex>
);
}
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,6 @@ export * from './AttachmentList';
export * from './AttachmentListPlaceholder';
export {QueryGraph} from './QueryGraph';
export type {QueryGraphProps} from './QueryGraph';
export {DataTypePopup} from './DataTypePopup';
export type {DataTypePopupProps} from './DataTypePopup';
export * from './MonacoDiffEditor';
15 changes: 15 additions & 0 deletions src/helpers/dataTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {ParameterType} from '../types/parameterWizard';

export const optionalDepth = (type: ParameterType) =>
type.optional ? Math.max(1, type.optionalLevel || 1) : 0;

export function formatType(type: ParameterType): string {
let result: string = type.name;
if ('struct' in type)
result += `<${type.struct.map((field) => `${field.key}: ${formatType(field.type)}`).join(', ')}>`;
else if ('type' in type) {
result += `<${Array.isArray(type.type) ? type.type.map((item) => (type.name === 'Enum' ? item.name : formatType(item as ParameterType))).join(', ') : formatType(type.type)}>`;
} else if (type.params?.length) result += `(${type.params.join(', ')})`;
for (const tag of type.tags || []) result = `Tagged<${result}, ${tag}>`;
return result + '?'.repeat(optionalDepth(type));
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export * from './types/errorTree';
export * from './types/queryStatistics';
export * from './types/queryResults';
export * from './types/queryGraph';
export * from './types/parameterWizard';
export * from './types/dataTypePopup';
export * from './types/queryTimeline';
export * from './types/queryExecutionPanel';
export * from './types/queriesSidebar';
Expand Down
4 changes: 4 additions & 0 deletions src/modules/ParameterWizard/ParameterWizard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.qp-parameter-wizard {
min-width: 0;
width: 100%;
}
Loading
Loading