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 ( + + +
{ + e.preventDefault(); + const validationError = validateRelativePath(relativePath); + setValidationError(validationError); + if (!validationError) { + handleSubmit(); + } + }} + > + + +
+ +
+ {response.isError && ( + + )} +
+ + +
+ ); +} 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 {