diff --git a/src/components/Errors/ResponseError/ResponseError.tsx b/src/components/Errors/ResponseError/ResponseError.tsx
index 56994d8fd..026311f08 100644
--- a/src/components/Errors/ResponseError/ResponseError.tsx
+++ b/src/components/Errors/ResponseError/ResponseError.tsx
@@ -17,7 +17,9 @@ export const ResponseError = ({
statusText = error;
}
if (error && typeof error === 'object') {
- if ('statusText' in error && typeof error.statusText === 'string') {
+ if ('data' in error && typeof error.data === 'string') {
+ statusText = error.data;
+ } else if ('statusText' in error && typeof error.statusText === 'string') {
statusText = error.statusText;
} else if ('message' in error && typeof error.message === 'string') {
statusText = error.message;
diff --git a/src/containers/Tenant/Schema/CreateDirectoryDialog/CreateDirectoryDialog.scss b/src/containers/Tenant/Schema/CreateDirectoryDialog/CreateDirectoryDialog.scss
new file mode 100644
index 000000000..fb39b3476
--- /dev/null
+++ b/src/containers/Tenant/Schema/CreateDirectoryDialog/CreateDirectoryDialog.scss
@@ -0,0 +1,14 @@
+.ydb-schema-create-directory-dialog {
+ &__label {
+ display: flex;
+ flex-direction: column;
+
+ margin-bottom: 8px;
+ }
+ &__description {
+ color: var(--g-color-text-secondary);
+ }
+ &__input-wrapper {
+ min-height: 48px;
+ }
+}
diff --git a/src/containers/Tenant/Schema/CreateDirectoryDialog/CreateDirectoryDialog.tsx b/src/containers/Tenant/Schema/CreateDirectoryDialog/CreateDirectoryDialog.tsx
new file mode 100644
index 000000000..301c7955b
--- /dev/null
+++ b/src/containers/Tenant/Schema/CreateDirectoryDialog/CreateDirectoryDialog.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+
+import {Dialog, TextInput} from '@gravity-ui/uikit';
+
+import {ResponseError} from '../../../../components/Errors/ResponseError';
+import {schemaApi} from '../../../../store/reducers/schema/schema';
+import {cn} from '../../../../utils/cn';
+import i18n from '../../i18n';
+
+import './CreateDirectoryDialog.scss';
+
+const b = cn('ydb-schema-create-directory-dialog');
+
+const relativePathInputId = 'relativePath';
+
+interface SchemaTreeProps {
+ open: boolean;
+ onClose: VoidFunction;
+ parentPath: string;
+ onSuccess: (value: string) => void;
+}
+
+function validateRelativePath(value: string) {
+ if (value && /\s/.test(value)) {
+ return i18n('schema.tree.dialog.whitespace');
+ }
+ return '';
+}
+
+export function CreateDirectoryDialog({open, onClose, parentPath, onSuccess}: SchemaTreeProps) {
+ const [validationError, setValidationError] = React.useState('');
+ const [relativePath, setRelativePath] = React.useState('');
+ const [create, response] = schemaApi.useCreateDirectoryMutation();
+
+ const resetErrors = () => {
+ setValidationError('');
+ response.reset();
+ };
+
+ const handleUpdate = (updated: string) => {
+ setRelativePath(updated);
+ resetErrors();
+ };
+
+ const handleClose = () => {
+ onClose();
+ setRelativePath('');
+ resetErrors();
+ };
+
+ const handleSubmit = () => {
+ const path = `${parentPath}/${relativePath}`;
+ create({
+ database: parentPath,
+ path,
+ })
+ .unwrap()
+ .then(() => {
+ handleClose();
+ onSuccess(relativePath);
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx
index 66e51e301..9d25acad1 100644
--- a/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx
+++ b/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx
@@ -1,3 +1,6 @@
+// todo: tableTree is very smart, so it is impossible to update it without re-render
+// It is need change NavigationTree to dump component and pass props from parent component
+// In this case we can store state of tree - uploaded entities, opened nodes, selected entity and so on
import React from 'react';
import {NavigationTree} from 'ydb-ui-components';
@@ -8,6 +11,7 @@ import {useQueryModes, useTypedDispatch} from '../../../../utils/hooks';
import {isChildlessPathType, mapPathTypeToNavigationTreeType} from '../../utils/schema';
import {getActions} from '../../utils/schemaActions';
import {getControls} from '../../utils/schemaControls';
+import {CreateDirectoryDialog} from '../CreateDirectoryDialog/CreateDirectoryDialog';
interface SchemaTreeProps {
rootPath: string;
@@ -19,10 +23,12 @@ interface SchemaTreeProps {
export function SchemaTree(props: SchemaTreeProps) {
const {rootPath, rootName, rootType, currentPath, onActivePathUpdate} = props;
-
const dispatch = useTypedDispatch();
const [_, setQueryMode] = useQueryModes();
+ const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false);
+ const [parentPath, setParentPath] = React.useState('');
+ const [schemaTreeKey, setSchemaTreeKey] = React.useState('');
const fetchPath = async (path: string) => {
const promise = dispatch(
@@ -49,7 +55,6 @@ export function SchemaTree(props: SchemaTreeProps) {
return childItems;
};
-
React.useEffect(() => {
// if the cached path is not in the current tree, show root
if (!currentPath?.startsWith(rootPath)) {
@@ -57,26 +62,50 @@ export function SchemaTree(props: SchemaTreeProps) {
}
}, [currentPath, onActivePathUpdate, rootPath]);
+ const handleSuccessSubmit = (relativePath: string) => {
+ const newPath = `${parentPath}/${relativePath}`;
+ onActivePathUpdate(newPath);
+ setSchemaTreeKey(newPath);
+ };
+
+ const handleCloseDialog = () => {
+ setCreateDirectoryOpen(false);
+ };
+
+ const handleOpenCreateDirectoryDialog = (value: string) => {
+ setParentPath(value);
+ setCreateDirectoryOpen(true);
+ };
return (
-
+
+
+
+
);
}
diff --git a/src/containers/Tenant/i18n/en.json b/src/containers/Tenant/i18n/en.json
index 8b358bb9a..1c1843945 100644
--- a/src/containers/Tenant/i18n/en.json
+++ b/src/containers/Tenant/i18n/en.json
@@ -40,5 +40,13 @@
"actions.selectQuery": "Select query...",
"actions.upsertQuery": "Upsert query...",
"actions.alterReplication": "Alter async replicaton...",
- "actions.dropReplication": "Drop async replicaton..."
+ "actions.dropReplication": "Drop async replicaton...",
+ "actions.createDirectory": "Create directory",
+ "schema.tree.dialog.placeholder": "Relative path",
+ "schema.tree.dialog.invalid": "Invalid path",
+ "schema.tree.dialog.whitespace": "Whitespace is not allowed",
+ "schema.tree.dialog.header": "Create directory",
+ "schema.tree.dialog.description": "Inside",
+ "schema.tree.dialog.buttonCancel": "Cancel",
+ "schema.tree.dialog.buttonApply": "Create"
}
diff --git a/src/containers/Tenant/utils/schemaActions.ts b/src/containers/Tenant/utils/schemaActions.ts
index f3b34ecf1..9f44501a0 100644
--- a/src/containers/Tenant/utils/schemaActions.ts
+++ b/src/containers/Tenant/utils/schemaActions.ts
@@ -29,6 +29,7 @@ import {
interface ActionsAdditionalEffects {
setQueryMode: (mode: QueryMode) => void;
setActivePath: (path: string) => void;
+ showCreateDirectoryDialog: (path: string) => void;
}
const bindActions = (
@@ -36,7 +37,7 @@ const bindActions = (
dispatch: React.Dispatch,
additionalEffects: ActionsAdditionalEffects,
) => {
- const {setActivePath, setQueryMode} = additionalEffects;
+ const {setActivePath, setQueryMode, showCreateDirectoryDialog} = additionalEffects;
const inputQuery = (tmpl: (path: string) => string, mode?: QueryMode) => () => {
if (mode) {
@@ -50,6 +51,9 @@ const bindActions = (
};
return {
+ createDirectory: () => {
+ showCreateDirectoryDialog(path);
+ },
createTable: inputQuery(createTableTemplate, 'script'),
createColumnTable: inputQuery(createColumnTableTemplate, 'script'),
createAsyncReplication: inputQuery(createAsyncReplicationTemplate, 'script'),
@@ -95,6 +99,7 @@ export const getActions =
const DIR_SET: ActionsSet = [
[copyItem],
+ [{text: i18n('actions.createDirectory'), action: actions.createDirectory}],
[
{text: i18n('actions.createTable'), action: actions.createTable},
{text: i18n('actions.createColumnTable'), action: actions.createColumnTable},
diff --git a/src/services/api.ts b/src/services/api.ts
index fc1e138e4..72927b593 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -635,6 +635,20 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
requestConfig: {signal},
});
}
+
+ createSchemaDirectory(database: string, path: string, {signal}: {signal?: AbortSignal} = {}) {
+ return this.post<{test: string}>(
+ this.getPath('/scheme/directory'),
+ {},
+ {
+ database,
+ path,
+ },
+ {
+ requestConfig: {signal},
+ },
+ );
+ }
}
export class YdbWebVersionAPI extends YdbEmbeddedAPI {
diff --git a/src/store/reducers/schema/schema.ts b/src/store/reducers/schema/schema.ts
index ae760fbd8..a32c3937b 100644
--- a/src/store/reducers/schema/schema.ts
+++ b/src/store/reducers/schema/schema.ts
@@ -41,6 +41,16 @@ export default schema;
export const schemaApi = api.injectEndpoints({
endpoints: (builder) => ({
+ createDirectory: builder.mutation({
+ queryFn: async ({database, path}, {signal}) => {
+ try {
+ const data = await window.api.createSchemaDirectory(database, path, {signal});
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ },
+ }),
getSchema: builder.query({
queryFn: async ({path}, {signal}) => {
try {