Skip to content

Commit a571b79

Browse files
committed
feat: add topic form
1 parent 203e728 commit a571b79

10 files changed

Lines changed: 1031 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
.ydb-create-topic-dialog {
2+
&__body {
3+
overflow-y: auto;
4+
5+
max-height: 70vh;
6+
}
7+
8+
&__form {
9+
display: flex;
10+
flex-direction: column;
11+
gap: var(--g-spacing-5);
12+
}
13+
14+
&__section {
15+
display: flex;
16+
flex-direction: column;
17+
gap: var(--g-spacing-3);
18+
}
19+
20+
&__section-title {
21+
margin: 0;
22+
}
23+
24+
&__row {
25+
display: flex;
26+
flex-direction: row;
27+
align-items: flex-start;
28+
gap: var(--g-spacing-3);
29+
30+
width: 100%;
31+
}
32+
33+
&__row-title {
34+
display: flex;
35+
flex: 4;
36+
align-items: center;
37+
gap: var(--g-spacing-1);
38+
39+
padding-top: 6px;
40+
41+
color: var(--g-color-text-primary);
42+
}
43+
44+
&__row-control {
45+
display: flex;
46+
flex: 6;
47+
flex-direction: column;
48+
gap: var(--g-spacing-1);
49+
50+
min-width: 0;
51+
}
52+
53+
&__row-control-line {
54+
display: flex;
55+
align-items: center;
56+
gap: var(--g-spacing-2);
57+
}
58+
59+
&__required-mark {
60+
color: var(--g-color-text-danger);
61+
}
62+
63+
&__incompatibility-warning {
64+
display: inline-flex;
65+
align-items: center;
66+
67+
cursor: help;
68+
69+
color: var(--g-color-text-warning);
70+
}
71+
72+
&__input-suffix {
73+
padding: 0 var(--g-spacing-2);
74+
75+
color: var(--g-color-text-secondary);
76+
}
77+
78+
&__hint {
79+
color: var(--g-color-text-secondary);
80+
}
81+
82+
&__api-error {
83+
white-space: pre-wrap;
84+
word-break: break-word;
85+
}
86+
87+
&__divider {
88+
margin: 0;
89+
}
90+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React from 'react';
2+
3+
import * as NiceModal from '@ebay/nice-modal-react';
4+
import {Button, Dialog, Text} from '@gravity-ui/uikit';
5+
import {zodResolver} from '@hookform/resolvers/zod';
6+
import {FormProvider, useForm} from 'react-hook-form';
7+
8+
import {createTopicApi} from '../../../../store/reducers/createTopic/createTopic';
9+
import {cn} from '../../../../utils/cn';
10+
import createToast from '../../../../utils/createToast';
11+
import {prepareErrorMessage} from '../../../../utils/prepareErrorMessage';
12+
13+
import {CREATE_TOPIC_DIALOG, DEFAULT_CREATE_TOPIC_VALUES} from './constants';
14+
import i18n from './i18n';
15+
import type {CreateTopicFormValues} from './schema';
16+
import {createTopicFormSchema, prepareCreateTopicPayload} from './schema';
17+
import {GeneralSection} from './sections/GeneralSection';
18+
import {ParametersSection} from './sections/ParametersSection';
19+
20+
import './CreateTopicDialog.scss';
21+
22+
const b = cn('ydb-create-topic-dialog');
23+
24+
interface CreateTopicDialogProps {
25+
database: string;
26+
open: boolean;
27+
onClose: () => void;
28+
}
29+
30+
function CreateTopicDialog({database, open, onClose}: CreateTopicDialogProps) {
31+
const [apiError, setApiError] = React.useState<string | null>(null);
32+
const [createTopic, {isLoading}] = createTopicApi.useCreateTopicMutation();
33+
34+
const form = useForm<CreateTopicFormValues>({
35+
defaultValues: DEFAULT_CREATE_TOPIC_VALUES,
36+
resolver: zodResolver(createTopicFormSchema),
37+
mode: 'onChange',
38+
});
39+
40+
const onSubmit = form.handleSubmit(async (values) => {
41+
setApiError(null);
42+
try {
43+
await createTopic(prepareCreateTopicPayload(values, database)).unwrap();
44+
createToast({
45+
name: 'create-topic-success',
46+
title: i18n('toast_created-title'),
47+
content: i18n('toast_created-content', {name: values.name}),
48+
autoHiding: 5000,
49+
isClosable: true,
50+
});
51+
onClose();
52+
} catch (error) {
53+
setApiError(prepareErrorMessage(error) || i18n('error_required'));
54+
}
55+
});
56+
57+
return (
58+
<Dialog open={open} onClose={onClose} size="m" className={b()}>
59+
<Dialog.Header caption={i18n('title_create-topic')} />
60+
<FormProvider {...form}>
61+
<form onSubmit={onSubmit}>
62+
<Dialog.Body className={b('body')}>
63+
<div className={b('form')}>
64+
<GeneralSection />
65+
<ParametersSection />
66+
{apiError ? (
67+
<Text color="danger" className={b('api-error')}>
68+
{apiError}
69+
</Text>
70+
) : null}
71+
</div>
72+
</Dialog.Body>
73+
<Dialog.Footer
74+
textButtonApply={i18n('action_apply')}
75+
textButtonCancel={i18n('action_cancel')}
76+
onClickButtonCancel={onClose}
77+
loading={isLoading}
78+
propsButtonApply={{
79+
type: 'submit',
80+
disabled: isLoading,
81+
}}
82+
/>
83+
</form>
84+
</FormProvider>
85+
</Dialog>
86+
);
87+
}
88+
89+
interface CreateTopicDialogNiceModalProps {
90+
database: string;
91+
}
92+
93+
const CreateTopicDialogNiceModal = NiceModal.create(
94+
({database}: CreateTopicDialogNiceModalProps) => {
95+
const modal = NiceModal.useModal();
96+
97+
const handleClose = React.useCallback(() => {
98+
modal.hide();
99+
modal.remove();
100+
}, [modal]);
101+
102+
return <CreateTopicDialog database={database} open={modal.visible} onClose={handleClose} />;
103+
},
104+
);
105+
106+
NiceModal.register(CREATE_TOPIC_DIALOG, CreateTopicDialogNiceModal);
107+
108+
export function openCreateTopicDialog(props: CreateTopicDialogNiceModalProps) {
109+
return NiceModal.show(CREATE_TOPIC_DIALOG, props);
110+
}
111+
112+
interface CreateTopicButtonProps {
113+
database: string;
114+
disabled?: boolean;
115+
}
116+
117+
export function CreateTopicButton({database, disabled}: CreateTopicButtonProps) {
118+
const onClick = React.useCallback(() => {
119+
openCreateTopicDialog({database});
120+
}, [database]);
121+
122+
return (
123+
<Button onClick={onClick} disabled={disabled}>
124+
{i18n('action_create-topic')}
125+
</Button>
126+
);
127+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
3+
import {TriangleExclamationFill} from '@gravity-ui/icons';
4+
import {HelpMark, Icon, Popover} from '@gravity-ui/uikit';
5+
6+
import {cn} from '../../../../utils/cn';
7+
8+
const b = cn('ydb-create-topic-dialog');
9+
10+
interface FormRowProps {
11+
title: React.ReactNode;
12+
note?: React.ReactNode;
13+
required?: boolean;
14+
htmlFor?: string;
15+
children: React.ReactNode;
16+
}
17+
18+
export function FormRow({title, note, required, htmlFor, children}: FormRowProps) {
19+
return (
20+
<div className={b('row')}>
21+
<label className={b('row-title')} htmlFor={htmlFor}>
22+
<span>
23+
{title}
24+
{required ? <span className={b('required-mark')}> *</span> : null}
25+
</span>
26+
{note ? <HelpMark iconSize="s">{note}</HelpMark> : null}
27+
</label>
28+
<div className={b('row-control')}>{children}</div>
29+
</div>
30+
);
31+
}
32+
33+
interface IncompatibilityWarningProps {
34+
content: React.ReactNode;
35+
}
36+
37+
export function IncompatibilityWarning({content}: IncompatibilityWarningProps) {
38+
return (
39+
<Popover content={content} placement={['top', 'bottom']}>
40+
<span className={b('incompatibility-warning')} tabIndex={0}>
41+
<Icon data={TriangleExclamationFill} size={16} />
42+
</span>
43+
</Popover>
44+
);
45+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type {
2+
CreateTopicAutoPartitioningStrategy,
3+
CreateTopicMeteringMode,
4+
} from '../../../../store/reducers/createTopic/createTopic';
5+
6+
import type {CreateTopicFormValues} from './schema';
7+
8+
export const CREATE_TOPIC_DIALOG = 'create-topic-dialog';
9+
10+
export const RETENTION_TYPES = ['time', 'size'] as const;
11+
export type RetentionType = (typeof RETENTION_TYPES)[number];
12+
13+
export const AUTO_PARTITIONING_MODES = ['scale-up', 'paused'] as const;
14+
export type AutoPartitioningMode = CreateTopicAutoPartitioningStrategy;
15+
16+
export const WRITE_QUOTAS_KB = [128, 512, 1024] as const;
17+
export const RETENTION_HOURS_OPTIONS = [1, 4, 12, 24] as const;
18+
19+
export const DEFAULT_METER_MODE: CreateTopicMeteringMode = 'unspecified';
20+
21+
export const STORAGE_LIMIT_MIN_GB = 50;
22+
export const STORAGE_LIMIT_MAX_GB = 400;
23+
export const STORAGE_LIMIT_STEP_GB = 1;
24+
export const MB_PER_GB = 1024;
25+
26+
export const DEFAULT_CREATE_TOPIC_VALUES: CreateTopicFormValues = {
27+
name: '',
28+
shards: 1,
29+
writeQuota: 1024,
30+
retentionType: 'time',
31+
retentionHours: 4,
32+
storageLimitMb: STORAGE_LIMIT_MIN_GB * MB_PER_GB,
33+
autoPartitioning: {
34+
enabled: false,
35+
mode: 'scale-up',
36+
minPartitions: 1,
37+
maxPartitions: 2,
38+
stabilizationWindow: 300,
39+
upUtilization: 90,
40+
downUtilization: 30,
41+
},
42+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"action_create-topic": "Create topic",
3+
"action_apply": "Create",
4+
"action_cancel": "Cancel",
5+
"action_enable": "Enable",
6+
7+
"title_create-topic": "Create topic",
8+
9+
"section_general-parameters": "General parameters",
10+
"section_stream-parameters": "Stream parameters",
11+
12+
"field_name": "Name",
13+
"context_field_name": "Minimum {{min}} characters",
14+
15+
"label_shard-write-quota": "Shard throughput",
16+
"context_shards-write-quota": "The maximum allowed write speed per segment",
17+
"label_throughput-info": "Total throughput: up to {{speed}}",
18+
"label_throughput-info-range": "Total throughput: from {{from}} to {{to}}",
19+
20+
"label_auto-partitioning": "Autopartitioning",
21+
"context_auto-partitioning": "Dynamic scaling of the topic depending on the write speed",
22+
"label_auto-partitioning-mode": "Autopartitioning mode",
23+
"context_auto-partitioning-mode": "Autopartitioning mode for the topic",
24+
"label_auto-partitioning-mode-restricted": "Autopartitioning cannot be enabled with size storage mode",
25+
"label_auto-partitioning-paused": "Paused",
26+
"label_auto-partitioning-scale-up": "Scale up",
27+
"label_auto-partitioning-settings": "Advanced settings",
28+
"label_auto-partitioning-stabilization-window": "Stabilization period",
29+
"context_auto-partitioning-stabilization-window": "The period of time after a load change when the number of segments will change",
30+
"label_auto-partitioning-up-utilization": "Upper utilization limit",
31+
"context_auto-partitioning-up-utilization": "The threshold at which the number of segments increases",
32+
"label_confirm-auto-partitioning-title": "Enable autopartitioning?",
33+
"label_confirm-auto-partitioning-message": "The Kafka protocol does not support topic auto-partitioning. If you enable auto-partitioning, you will not be able to work with the topics you create using the Kafka protocol. You can not disable auto-partitioning for a specific topic.",
34+
35+
"label_shards": "Shards",
36+
"context_shards": "Number of segments in the stream",
37+
"label_shards-info": "The number of shards cannot be reduced",
38+
"label_min": "Min",
39+
"label_max": "Max",
40+
41+
"label_data-storage-options": "Data storage mode",
42+
"context_data-storage-options": "Data storage limits: by storage time, by storage volume",
43+
"label_data-storage-time-limit": "Time Limit",
44+
"label_data-storage-size-limit": "Size Limit",
45+
"label_data-storage-options-restricted": "Size storage mode cannot be enabled with autopartitioning",
46+
"label_data-storage-note": "The last {{total}} GB ({{size}} GB x {{count}} segments) of data will be stored for 7 days",
47+
"label_retention-unavailable": "Available only with {{speed}} throughput",
48+
"label_retention-period_1h": "1 hour",
49+
"label_retention-period_4h": "4 hours",
50+
"label_retention-period_12h": "12 hours",
51+
"label_retention-period_24h": "1 day",
52+
53+
"unit_seconds-short": "sec",
54+
"unit_percent-short": "%",
55+
"unit_kb-per-second": "{{value}} KB/s",
56+
"unit_gb": "{{value}} GB",
57+
58+
"error_required": "The field is required",
59+
"error_name-min": "Minimum {{min}} characters",
60+
"error_min-greater-max": "Min must be less than max",
61+
"error_max-less-min": "Max must be greater than min",
62+
"error_positive-int": "Value must be a positive integer",
63+
64+
"toast_created-title": "Topic created",
65+
"toast_created-content": "Topic \"{{name}}\" has been created"
66+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {registerKeysets} from '../../../../../utils/i18n';
2+
3+
import en from './en.json';
4+
5+
const COMPONENT = 'ydb-create-topic-dialog';
6+
7+
export default registerKeysets(COMPONENT, {en});

0 commit comments

Comments
 (0)