Skip to content

Commit d1902d4

Browse files
walbornRaubzeug
andauthored
feat: add creating directory through context menu in navigation tree (#958)
Co-authored-by: Elena Makarova <[email protected]>
1 parent 459a3dd commit d1902d4

File tree

8 files changed

+222
-25
lines changed

8 files changed

+222
-25
lines changed

src/components/Errors/ResponseError/ResponseError.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export const ResponseError = ({
1717
statusText = error;
1818
}
1919
if (error && typeof error === 'object') {
20-
if ('statusText' in error && typeof error.statusText === 'string') {
20+
if ('data' in error && typeof error.data === 'string') {
21+
statusText = error.data;
22+
} else if ('statusText' in error && typeof error.statusText === 'string') {
2123
statusText = error.statusText;
2224
} else if ('message' in error && typeof error.message === 'string') {
2325
statusText = error.message;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.ydb-schema-create-directory-dialog {
2+
&__label {
3+
display: flex;
4+
flex-direction: column;
5+
6+
margin-bottom: 8px;
7+
}
8+
&__description {
9+
color: var(--g-color-text-secondary);
10+
}
11+
&__input-wrapper {
12+
min-height: 48px;
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React from 'react';
2+
3+
import {Dialog, TextInput} from '@gravity-ui/uikit';
4+
5+
import {ResponseError} from '../../../../components/Errors/ResponseError';
6+
import {schemaApi} from '../../../../store/reducers/schema/schema';
7+
import {cn} from '../../../../utils/cn';
8+
import i18n from '../../i18n';
9+
10+
import './CreateDirectoryDialog.scss';
11+
12+
const b = cn('ydb-schema-create-directory-dialog');
13+
14+
const relativePathInputId = 'relativePath';
15+
16+
interface SchemaTreeProps {
17+
open: boolean;
18+
onClose: VoidFunction;
19+
parentPath: string;
20+
onSuccess: (value: string) => void;
21+
}
22+
23+
function validateRelativePath(value: string) {
24+
if (value && /\s/.test(value)) {
25+
return i18n('schema.tree.dialog.whitespace');
26+
}
27+
return '';
28+
}
29+
30+
export function CreateDirectoryDialog({open, onClose, parentPath, onSuccess}: SchemaTreeProps) {
31+
const [validationError, setValidationError] = React.useState('');
32+
const [relativePath, setRelativePath] = React.useState('');
33+
const [create, response] = schemaApi.useCreateDirectoryMutation();
34+
35+
const resetErrors = () => {
36+
setValidationError('');
37+
response.reset();
38+
};
39+
40+
const handleUpdate = (updated: string) => {
41+
setRelativePath(updated);
42+
resetErrors();
43+
};
44+
45+
const handleClose = () => {
46+
onClose();
47+
setRelativePath('');
48+
resetErrors();
49+
};
50+
51+
const handleSubmit = () => {
52+
const path = `${parentPath}/${relativePath}`;
53+
create({
54+
database: parentPath,
55+
path,
56+
})
57+
.unwrap()
58+
.then(() => {
59+
handleClose();
60+
onSuccess(relativePath);
61+
});
62+
};
63+
64+
return (
65+
<Dialog open={open} onClose={handleClose} size="s">
66+
<Dialog.Header caption={i18n('schema.tree.dialog.header')} />
67+
<form
68+
onSubmit={(e) => {
69+
e.preventDefault();
70+
const validationError = validateRelativePath(relativePath);
71+
setValidationError(validationError);
72+
if (!validationError) {
73+
handleSubmit();
74+
}
75+
}}
76+
>
77+
<Dialog.Body>
78+
<label htmlFor={relativePathInputId} className={b('label')}>
79+
<span className={b('description')}>
80+
{i18n('schema.tree.dialog.description')}
81+
</span>
82+
{`${parentPath}/`}
83+
</label>
84+
<div className={b('input-wrapper')}>
85+
<TextInput
86+
placeholder={i18n('schema.tree.dialog.placeholder')}
87+
value={relativePath}
88+
onUpdate={handleUpdate}
89+
autoFocus
90+
hasClear
91+
autoComplete={false}
92+
disabled={response.isLoading}
93+
validationState={validationError ? 'invalid' : undefined}
94+
id={relativePathInputId}
95+
errorMessage={validationError}
96+
/>
97+
</div>
98+
{response.isError && (
99+
<ResponseError
100+
error={response.error}
101+
defaultMessage={i18n('schema.tree.dialog.invalid')}
102+
/>
103+
)}
104+
</Dialog.Body>
105+
<Dialog.Footer
106+
loading={response.isLoading}
107+
textButtonApply={i18n('schema.tree.dialog.buttonApply')}
108+
textButtonCancel={i18n('schema.tree.dialog.buttonCancel')}
109+
onClickButtonCancel={handleClose}
110+
propsButtonApply={{type: 'submit'}}
111+
/>
112+
</form>
113+
</Dialog>
114+
);
115+
}

src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx

+51-22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// todo: tableTree is very smart, so it is impossible to update it without re-render
2+
// It is need change NavigationTree to dump component and pass props from parent component
3+
// In this case we can store state of tree - uploaded entities, opened nodes, selected entity and so on
14
import React from 'react';
25

36
import {NavigationTree} from 'ydb-ui-components';
@@ -8,6 +11,7 @@ import {useQueryModes, useTypedDispatch} from '../../../../utils/hooks';
811
import {isChildlessPathType, mapPathTypeToNavigationTreeType} from '../../utils/schema';
912
import {getActions} from '../../utils/schemaActions';
1013
import {getControls} from '../../utils/schemaControls';
14+
import {CreateDirectoryDialog} from '../CreateDirectoryDialog/CreateDirectoryDialog';
1115

1216
interface SchemaTreeProps {
1317
rootPath: string;
@@ -19,10 +23,12 @@ interface SchemaTreeProps {
1923

2024
export function SchemaTree(props: SchemaTreeProps) {
2125
const {rootPath, rootName, rootType, currentPath, onActivePathUpdate} = props;
22-
2326
const dispatch = useTypedDispatch();
2427

2528
const [_, setQueryMode] = useQueryModes();
29+
const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false);
30+
const [parentPath, setParentPath] = React.useState('');
31+
const [schemaTreeKey, setSchemaTreeKey] = React.useState('');
2632

2733
const fetchPath = async (path: string) => {
2834
const promise = dispatch(
@@ -49,34 +55,57 @@ export function SchemaTree(props: SchemaTreeProps) {
4955

5056
return childItems;
5157
};
52-
5358
React.useEffect(() => {
5459
// if the cached path is not in the current tree, show root
5560
if (!currentPath?.startsWith(rootPath)) {
5661
onActivePathUpdate(rootPath);
5762
}
5863
}, [currentPath, onActivePathUpdate, rootPath]);
5964

65+
const handleSuccessSubmit = (relativePath: string) => {
66+
const newPath = `${parentPath}/${relativePath}`;
67+
onActivePathUpdate(newPath);
68+
setSchemaTreeKey(newPath);
69+
};
70+
71+
const handleCloseDialog = () => {
72+
setCreateDirectoryOpen(false);
73+
};
74+
75+
const handleOpenCreateDirectoryDialog = (value: string) => {
76+
setParentPath(value);
77+
setCreateDirectoryOpen(true);
78+
};
6079
return (
61-
<NavigationTree
62-
rootState={{
63-
path: rootPath,
64-
name: rootName,
65-
type: mapPathTypeToNavigationTreeType(rootType),
66-
collapsed: false,
67-
}}
68-
fetchPath={fetchPath}
69-
getActions={getActions(dispatch, {
70-
setActivePath: onActivePathUpdate,
71-
setQueryMode,
72-
})}
73-
renderAdditionalNodeElements={getControls(dispatch, {
74-
setActivePath: onActivePathUpdate,
75-
})}
76-
activePath={currentPath}
77-
onActivePathUpdate={onActivePathUpdate}
78-
cache={false}
79-
virtualize
80-
/>
80+
<React.Fragment>
81+
<CreateDirectoryDialog
82+
onClose={handleCloseDialog}
83+
open={createDirectoryOpen}
84+
parentPath={parentPath}
85+
onSuccess={handleSuccessSubmit}
86+
/>
87+
<NavigationTree
88+
key={schemaTreeKey}
89+
rootState={{
90+
path: rootPath,
91+
name: rootName,
92+
type: mapPathTypeToNavigationTreeType(rootType),
93+
collapsed: false,
94+
}}
95+
fetchPath={fetchPath}
96+
getActions={getActions(dispatch, {
97+
setActivePath: onActivePathUpdate,
98+
setQueryMode,
99+
showCreateDirectoryDialog: handleOpenCreateDirectoryDialog,
100+
})}
101+
renderAdditionalNodeElements={getControls(dispatch, {
102+
setActivePath: onActivePathUpdate,
103+
})}
104+
activePath={currentPath}
105+
onActivePathUpdate={onActivePathUpdate}
106+
cache={false}
107+
virtualize
108+
/>
109+
</React.Fragment>
81110
);
82111
}

src/containers/Tenant/i18n/en.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,13 @@
4040
"actions.selectQuery": "Select query...",
4141
"actions.upsertQuery": "Upsert query...",
4242
"actions.alterReplication": "Alter async replicaton...",
43-
"actions.dropReplication": "Drop async replicaton..."
43+
"actions.dropReplication": "Drop async replicaton...",
44+
"actions.createDirectory": "Create directory",
45+
"schema.tree.dialog.placeholder": "Relative path",
46+
"schema.tree.dialog.invalid": "Invalid path",
47+
"schema.tree.dialog.whitespace": "Whitespace is not allowed",
48+
"schema.tree.dialog.header": "Create directory",
49+
"schema.tree.dialog.description": "Inside",
50+
"schema.tree.dialog.buttonCancel": "Cancel",
51+
"schema.tree.dialog.buttonApply": "Create"
4452
}

src/containers/Tenant/utils/schemaActions.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ import {
2929
interface ActionsAdditionalEffects {
3030
setQueryMode: (mode: QueryMode) => void;
3131
setActivePath: (path: string) => void;
32+
showCreateDirectoryDialog: (path: string) => void;
3233
}
3334

3435
const bindActions = (
3536
path: string,
3637
dispatch: React.Dispatch<any>,
3738
additionalEffects: ActionsAdditionalEffects,
3839
) => {
39-
const {setActivePath, setQueryMode} = additionalEffects;
40+
const {setActivePath, setQueryMode, showCreateDirectoryDialog} = additionalEffects;
4041

4142
const inputQuery = (tmpl: (path: string) => string, mode?: QueryMode) => () => {
4243
if (mode) {
@@ -50,6 +51,9 @@ const bindActions = (
5051
};
5152

5253
return {
54+
createDirectory: () => {
55+
showCreateDirectoryDialog(path);
56+
},
5357
createTable: inputQuery(createTableTemplate, 'script'),
5458
createColumnTable: inputQuery(createColumnTableTemplate, 'script'),
5559
createAsyncReplication: inputQuery(createAsyncReplicationTemplate, 'script'),
@@ -95,6 +99,7 @@ export const getActions =
9599

96100
const DIR_SET: ActionsSet = [
97101
[copyItem],
102+
[{text: i18n('actions.createDirectory'), action: actions.createDirectory}],
98103
[
99104
{text: i18n('actions.createTable'), action: actions.createTable},
100105
{text: i18n('actions.createColumnTable'), action: actions.createColumnTable},

src/services/api.ts

+14
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,20 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
635635
requestConfig: {signal},
636636
});
637637
}
638+
639+
createSchemaDirectory(database: string, path: string, {signal}: {signal?: AbortSignal} = {}) {
640+
return this.post<{test: string}>(
641+
this.getPath('/scheme/directory'),
642+
{},
643+
{
644+
database,
645+
path,
646+
},
647+
{
648+
requestConfig: {signal},
649+
},
650+
);
651+
}
638652
}
639653

640654
export class YdbWebVersionAPI extends YdbEmbeddedAPI {

src/store/reducers/schema/schema.ts

+10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ export default schema;
4141

4242
export const schemaApi = api.injectEndpoints({
4343
endpoints: (builder) => ({
44+
createDirectory: builder.mutation<unknown, {database: string; path: string}>({
45+
queryFn: async ({database, path}, {signal}) => {
46+
try {
47+
const data = await window.api.createSchemaDirectory(database, path, {signal});
48+
return {data};
49+
} catch (error) {
50+
return {error};
51+
}
52+
},
53+
}),
4454
getSchema: builder.query<TEvDescribeSchemeResult & {partial?: boolean}, {path: string}>({
4555
queryFn: async ({path}, {signal}) => {
4656
try {

0 commit comments

Comments
 (0)