diff --git a/src/i18n-keysets/collections/en.json b/src/i18n-keysets/collections/en.json index fbba7f869b..ee7bf89f65 100644 --- a/src/i18n-keysets/collections/en.json +++ b/src/i18n-keysets/collections/en.json @@ -14,6 +14,8 @@ "action_create-workbook-hint": "Stores related connections, datasets, charts, and dashboards", "action_delete": "Delete", "action_edit": "Edit", + "action_export": "Export", + "action_import": "Import workbook", "action_move": "Move", "action_reset-all": "Reset all", "action_select": "Select", diff --git a/src/i18n-keysets/collections/ru.json b/src/i18n-keysets/collections/ru.json index dee0eeb79d..ccf56e8d8f 100644 --- a/src/i18n-keysets/collections/ru.json +++ b/src/i18n-keysets/collections/ru.json @@ -14,6 +14,8 @@ "action_create-workbook-hint": "Содержит подключения, датасеты, чарты и дашборды", "action_delete": "Удалить", "action_edit": "Редактировать", + "action_export": "Экспорт", + "action_import": "Импорт воркбука", "action_move": "Переместить", "action_reset-all": "Снять все", "action_select": "Выбрать", diff --git a/src/i18n-keysets/new-workbooks/en.json b/src/i18n-keysets/new-workbooks/en.json index 1be278040f..fd1117f92d 100644 --- a/src/i18n-keysets/new-workbooks/en.json +++ b/src/i18n-keysets/new-workbooks/en.json @@ -11,6 +11,7 @@ "action_delete": "Delete", "action_duplicate": "Duplicate", "action_edit": "Edit", + "action_export": "Export", "action_move": "Move", "action_rename": "Rename", "action_retry": "Retry", diff --git a/src/i18n-keysets/new-workbooks/ru.json b/src/i18n-keysets/new-workbooks/ru.json index 5636127787..4381c5cf3e 100644 --- a/src/i18n-keysets/new-workbooks/ru.json +++ b/src/i18n-keysets/new-workbooks/ru.json @@ -11,6 +11,7 @@ "action_delete": "Удалить", "action_duplicate": "Дублировать", "action_edit": "Редактировать", + "action_export": "Экспорт", "action_move": "Переместить", "action_rename": "Переименовать", "action_retry": "Обновить", diff --git a/src/shared/endpoints/constants/opensource.ts b/src/shared/endpoints/constants/opensource.ts index 996259f47f..9b3aadfb05 100644 --- a/src/shared/endpoints/constants/opensource.ts +++ b/src/shared/endpoints/constants/opensource.ts @@ -9,6 +9,8 @@ export const opensourceEndpoints = { charts: process.env.CHARTS_ENDPOINT || '/', uploader: process.env.UI_UPLOADER_ENDPOINT || '/uploader', + + transfer: process.env.TRANSFER_ENDPOINT, }, ui: { gateway: process.env.UI_GATEWAY_ENDPOINT || '/gateway', @@ -37,6 +39,8 @@ export const opensourceEndpoints = { charts: process.env.CHARTS_ENDPOINT || '/', uploader: process.env.UI_UPLOADER_ENDPOINT || '/uploader', + + transfer: process.env.TRANSFER_ENDPOINT, }, ui: { gateway: process.env.UI_GATEWAY_ENDPOINT || '/gateway', diff --git a/src/shared/endpoints/schema/opensource.ts b/src/shared/endpoints/schema/opensource.ts index 739b931a28..a6d25a62e5 100644 --- a/src/shared/endpoints/schema/opensource.ts +++ b/src/shared/endpoints/schema/opensource.ts @@ -13,6 +13,9 @@ export const opensourceEndpoints = { us: { endpoint: endpoints.development.api.us, }, + transfer: { + endpoint: endpoints.development.api.transfer, + }, }, production: { bi: { @@ -26,5 +29,8 @@ export const opensourceEndpoints = { us: { endpoint: endpoints.production.api.us, }, + transfer: { + endpoint: endpoints.development.api.transfer, + }, }, }; diff --git a/src/shared/schema/simple-schema.ts b/src/shared/schema/simple-schema.ts index a6b8668ad8..3995fd1ecb 100644 --- a/src/shared/schema/simple-schema.ts +++ b/src/shared/schema/simple-schema.ts @@ -3,6 +3,7 @@ import {getTypedApiFactory} from '@gravity-ui/gateway'; import bi from './bi'; import biConverter from './bi-converter'; import extensions from './extensions'; +import transfer from './transfer'; import us from './us'; // Scheme for all local requests except mix @@ -11,6 +12,7 @@ export const simpleSchema = { bi, biConverter, extensions, + transfer, }; export const getTypedApi = getTypedApiFactory<{root: typeof simpleSchema}>(); diff --git a/src/shared/schema/transfer/actions/exports.ts b/src/shared/schema/transfer/actions/exports.ts new file mode 100644 index 0000000000..a1efc4a9fd --- /dev/null +++ b/src/shared/schema/transfer/actions/exports.ts @@ -0,0 +1,24 @@ +import {createAction} from '../../gateway-utils'; +import type { + GetExportStatusArgs, + GetExportStatusResponse, + StartExportArgs, + StartExportResponse, +} from '../types'; + +export const exportActions = { + startExport: createAction({ + method: 'POST', + path: () => '/workbooks/export', + params: ({workbookId}) => ({ + body: { + workbookId, + }, + }), + }), + + getExportStatus: createAction({ + method: 'GET', + path: ({exportId}) => `/workbooks/export/${exportId}`, + }), +}; diff --git a/src/shared/schema/transfer/actions/imports.ts b/src/shared/schema/transfer/actions/imports.ts new file mode 100644 index 0000000000..7a3676a0f0 --- /dev/null +++ b/src/shared/schema/transfer/actions/imports.ts @@ -0,0 +1,9 @@ +import {createAction} from '../../gateway-utils'; +import type {GetImportStatusArgs, GetImportStatusResponse} from '../types'; + +export const importActions = { + getImportStatus: createAction({ + method: 'GET', + path: ({importId}) => `/workbooks/import/${importId}`, + }), +}; diff --git a/src/shared/schema/transfer/actions/index.ts b/src/shared/schema/transfer/actions/index.ts new file mode 100644 index 0000000000..a3d28c95fd --- /dev/null +++ b/src/shared/schema/transfer/actions/index.ts @@ -0,0 +1,7 @@ +import {exportActions} from './exports'; +import {importActions} from './imports'; + +export const actions = { + ...exportActions, + ...importActions, +}; diff --git a/src/shared/schema/transfer/index.ts b/src/shared/schema/transfer/index.ts new file mode 100644 index 0000000000..3086823ed4 --- /dev/null +++ b/src/shared/schema/transfer/index.ts @@ -0,0 +1,9 @@ +import {getServiceEndpoints} from '../../endpoints/schema'; + +import {actions} from './actions'; + +export default { + actions, + endpoints: getServiceEndpoints('transfer'), + serviceName: 'transfer', +}; diff --git a/src/shared/schema/transfer/types/exports.ts b/src/shared/schema/transfer/types/exports.ts new file mode 100644 index 0000000000..7b29c1cf6a --- /dev/null +++ b/src/shared/schema/transfer/types/exports.ts @@ -0,0 +1,17 @@ +export type StartExportArgs = { + workbookId: string; +}; + +export type StartExportResponse = { + exportId: string; +}; + +export type GetExportStatusArgs = { + exportId: string; +}; + +export type GetExportStatusResponse = { + exportId: string; + status: 'pending' | 'success' | 'error'; + data: Record; +}; diff --git a/src/shared/schema/transfer/types/imports.ts b/src/shared/schema/transfer/types/imports.ts new file mode 100644 index 0000000000..94e94e15da --- /dev/null +++ b/src/shared/schema/transfer/types/imports.ts @@ -0,0 +1,9 @@ +export type GetImportStatusArgs = { + importId: string; +}; + +export type GetImportStatusResponse = { + importId: string; + status: 'pending' | 'success' | 'error'; + progress: number; +}; diff --git a/src/shared/schema/transfer/types/index.ts b/src/shared/schema/transfer/types/index.ts new file mode 100644 index 0000000000..c866d93efe --- /dev/null +++ b/src/shared/schema/transfer/types/index.ts @@ -0,0 +1,2 @@ +export * from './exports'; +export * from './imports'; diff --git a/src/ui/components/CollectionsStructure/ExportWorkbookDialog.tsx b/src/ui/components/CollectionsStructure/ExportWorkbookDialog.tsx new file mode 100644 index 0000000000..a205c04d6a --- /dev/null +++ b/src/ui/components/CollectionsStructure/ExportWorkbookDialog.tsx @@ -0,0 +1,92 @@ +import React from 'react'; + +import {Dialog} from '@gravity-ui/uikit'; + +import DialogManager from '../../components/DialogManager/DialogManager'; +import {getSdk} from '../../libs/schematic-sdk'; + +export type Props = { + open: boolean; + workbookId: string; + workbookTitle: string; + onClose: () => void; +}; + +export const DIALOG_EXPORT_WORKBOOK = Symbol('DIALOG_EXPORT_WORKBOOK'); + +export type OpenDialogExportWorkbookArgs = { + id: typeof DIALOG_EXPORT_WORKBOOK; + props: Props; +}; + +export const ExportWorkbookDialog: React.FC = ({ + open, + workbookId, + workbookTitle, + onClose, +}) => { + const [isLoading, setIsLoading] = React.useState(false); + + const startExport = React.useCallback(() => { + setIsLoading(true); + + getSdk() + .transfer.startExport({workbookId}) + .then(async ({exportId}) => { + let isReady = false; + while (!isReady) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + + try { + const exportWorkbook = await getSdk().transfer.getExportStatus({exportId}); + + if (exportWorkbook.status === 'success') { + isReady = true; + setIsLoading(false); + + // Create blob link to download + const url = window.URL.createObjectURL( + new Blob([JSON.stringify(exportWorkbook.data)]), + ); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `export-${exportId}.json`); + + // Append to html link element page + document.body.appendChild(link); + + // Start download (close modal TODO: use ref) + link.click(); + + // Clean up and remove the link + link.parentNode?.removeChild(link); + } + } catch (err) {} + } + }); + }, [workbookId]); + + return ( + + + +
Export "{workbookTitle}"?
+
It may take a few minutes, don't close the dialog.
+
+ +
+ ); +}; + +DialogManager.registerDialog(DIALOG_EXPORT_WORKBOOK, ExportWorkbookDialog); diff --git a/src/ui/components/CollectionsStructure/ImportWorkbookDialog.tsx b/src/ui/components/CollectionsStructure/ImportWorkbookDialog.tsx new file mode 100644 index 0000000000..783d64886f --- /dev/null +++ b/src/ui/components/CollectionsStructure/ImportWorkbookDialog.tsx @@ -0,0 +1,112 @@ +import React from 'react'; + +import {Dialog, Progress, TextInput} from '@gravity-ui/uikit'; +import axios from 'axios'; + +import DialogManager from '../../components/DialogManager/DialogManager'; +import {getSdk} from '../../libs/schematic-sdk'; + +export type Props = { + open: boolean; + initialCollectionId?: string | null; + onClose: () => void; +}; + +export const DIALOG_IMPORT_WORKBOOK = Symbol('DIALOG_IMPORT_WORKBOOK'); + +export type OpenDialogImportWorkbookArgs = { + id: typeof DIALOG_IMPORT_WORKBOOK; + props: Props; +}; + +export const ImportWorkbookDialog: React.FC = ({ + open, + // initialCollectionId = null, + onClose, +}) => { + const [progress, setProgress] = React.useState(0); + const [title, setTitle] = React.useState(); + const [file, setFile] = React.useState(); + + const handleUploadFile = React.useCallback((event) => { + setFile(event.target.files[0]); + }, []); + + const [isLoading, setIsLoading] = React.useState(false); + + const startImport = React.useCallback(() => { + if (title && file) { + setIsLoading(true); + + const url = 'http://localhost:3001/workbooks/import'; + const formData = new FormData(); + formData.append('file', file); + formData.append('fileName', file.name); + formData.append('title', title); + const config = { + headers: { + 'content-type': 'multipart/form-data', + }, + }; + axios + .post(url, formData, config) + .then(async (response) => { + let isReady = false; + while (!isReady) { + await new Promise((resolve) => { + setTimeout(resolve, 3000); + }); + + try { + const importWorkbook = await getSdk().transfer.getImportStatus({ + importId: response.data.importId, + }); + + setProgress(importWorkbook.progress); + + if (importWorkbook.status === 'success') { + isReady = true; + window.location.reload(); + onClose(); + } + } catch (err) {} + } + }); + } + }, [file, onClose, title]); + + return ( + + + +
Title:
+
+ +
+
+
Upload the export file:
+
+ +
+ {isLoading ? ( + +
+ +
+ ) : null} +
+ +
+ ); +}; + +DialogManager.registerDialog(DIALOG_IMPORT_WORKBOOK, ImportWorkbookDialog); diff --git a/src/ui/components/CollectionsStructure/index.ts b/src/ui/components/CollectionsStructure/index.ts index 9aee017db4..1121b052f7 100644 --- a/src/ui/components/CollectionsStructure/index.ts +++ b/src/ui/components/CollectionsStructure/index.ts @@ -25,3 +25,5 @@ export { CreateEntryInWorkbookDialog, DIALOG_CREATE_ENTRY_IN_WORKBOOK, } from './CreateEntryInWorkbookDialog'; +export {ExportWorkbookDialog, DIALOG_EXPORT_WORKBOOK} from './ExportWorkbookDialog'; +export {ImportWorkbookDialog, DIALOG_IMPORT_WORKBOOK} from './ImportWorkbookDialog'; diff --git a/src/ui/store/actions/openDialogTypes.ts b/src/ui/store/actions/openDialogTypes.ts index 651f0b4ecd..733ae63631 100644 --- a/src/ui/store/actions/openDialogTypes.ts +++ b/src/ui/store/actions/openDialogTypes.ts @@ -32,6 +32,8 @@ import type {OpenDialogMoveCollectionsWorkbooksArgs} from '../../components/Coll import type {OpenDialogMoveCollectionArgs} from '../../components/CollectionsStructure/MoveCollectionDialog'; import type {OpenDialogMoveWorkbookArgs} from '../../components/CollectionsStructure/MoveWorkbookDialog'; import type {OpenDialogCopyWorkbookArgs} from '../../components/CollectionsStructure/CopyWorkbookDialog'; +import type {OpenDialogExportWorkbookArgs} from '../../components/CollectionsStructure/ExportWorkbookDialog'; +import type {OpenDialogImportWorkbookArgs} from '../../components/CollectionsStructure/ImportWorkbookDialog'; import type {OpenDialogMigrateEntryToWorkbookArgs} from '../../components/CollectionsStructure/MigrateEntryToWorkbookDialog'; import type {OpenDialogEditWorkbookArgs} from '../../components/CollectionsStructure/EditWorkbookDialog'; import type {OpenDialogCreateWorkbookArgs} from '../../components/CollectionsStructure/CreateWorkbookDialog'; @@ -109,4 +111,6 @@ export type OpenDialogArgs = | OpenDialogIamAccessArgs | OpenDialogCreateEntryInWorkbookArgs | OpenDialogTooltipSettingsArgs - | OpenDialogChangeDatasetFieldsArgs; + | OpenDialogChangeDatasetFieldsArgs + | OpenDialogExportWorkbookArgs + | OpenDialogImportWorkbookArgs; diff --git a/src/ui/units/collections/components/CollectionActions/CollectionActions.tsx b/src/ui/units/collections/components/CollectionActions/CollectionActions.tsx index 7fab65896b..a267980992 100644 --- a/src/ui/units/collections/components/CollectionActions/CollectionActions.tsx +++ b/src/ui/units/collections/components/CollectionActions/CollectionActions.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {ArrowRight, ChevronDown, LockOpen, TrashBin} from '@gravity-ui/icons'; +import {ArrowRight, ChevronDown, FileArrowUp, LockOpen, TrashBin} from '@gravity-ui/icons'; import type {DropdownMenuItem, DropdownMenuItemMixed} from '@gravity-ui/uikit'; import {Button, DropdownMenu, Icon, Tooltip} from '@gravity-ui/uikit'; import type {SVGIconData} from '@gravity-ui/uikit/build/esm/components/Icon/types'; @@ -33,6 +33,7 @@ export type Props = { onCreateWorkbookClick: () => void; onEditAccessClick: () => void; onMoveClick: () => void; + onImportWorkbookClick: () => void; onDeleteClick: () => void; }; @@ -46,6 +47,7 @@ export const CollectionActions = React.memo( onCreateWorkbookClick, onEditAccessClick, onMoveClick, + onImportWorkbookClick, onDeleteClick, }) => { const collection = useSelector(selectCollection); @@ -147,6 +149,11 @@ export const CollectionActions = React.memo( }); } + createActionItems.push({ + action: onImportWorkbookClick, + text: , + }); + const otherActions: DropdownMenuItem[] = []; if (collection && collection.permissions?.delete) { diff --git a/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx b/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx index b176c5ef1d..175a731d63 100644 --- a/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx +++ b/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {ArrowRight, Copy, LockOpen, PencilToLine, TrashBin} from '@gravity-ui/icons'; +import {ArrowRight, Copy, FileArrowDown, LockOpen, PencilToLine, TrashBin} from '@gravity-ui/icons'; import type {DropdownMenuItem} from '@gravity-ui/uikit'; import {I18n} from 'i18n'; import {useDispatch} from 'react-redux'; @@ -19,6 +19,7 @@ import { DIALOG_DELETE_WORKBOOK, DIALOG_EDIT_COLLECTION, DIALOG_EDIT_WORKBOOK, + DIALOG_EXPORT_WORKBOOK, DIALOG_MOVE_COLLECTION, DIALOG_MOVE_WORKBOOK, } from '../../../../../components/CollectionsStructure'; @@ -246,6 +247,25 @@ export const useActions = ({fetchStructureItems, onCloseMoveDialog}: UseActionsA }); } + actions.push({ + text: , + action: () => { + dispatch( + openDialog({ + id: DIALOG_EXPORT_WORKBOOK, + props: { + open: true, + workbookId: item.workbookId, + workbookTitle: item.title, + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); + }, + }); + if (collectionsAccessEnabled && item.permissions.listAccessBindings) { actions.push({ text: , diff --git a/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx b/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx index b24f9f31b3..4f2f3ddd0b 100644 --- a/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx +++ b/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx @@ -21,6 +21,7 @@ import { DIALOG_CREATE_COLLECTION, DIALOG_DELETE_COLLECTION, DIALOG_EDIT_COLLECTION, + DIALOG_IMPORT_WORKBOOK, DIALOG_MOVE_COLLECTION, } from '../../../../../components/CollectionsStructure'; import {DIALOG_IAM_ACCESS} from '../../../../../components/IamAccessDialog'; @@ -291,6 +292,22 @@ export const useLayout = ({ ); } }} + onImportWorkbookClick={() => { + dispatch( + openDialog({ + id: DIALOG_IMPORT_WORKBOOK, + props: { + open: true, + initialCollectionId: collection + ? collection.parentId + : null, + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); + }} onEditAccessClick={() => { if (collectionsAccessEnabled && curCollectionId && collection) { dispatch( diff --git a/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx b/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx index 66a082b33d..ce543063a6 100644 --- a/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx +++ b/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import {ArrowRight, Copy, LockOpen, TrashBin} from '@gravity-ui/icons'; +import {ArrowRight, Copy, FileArrowDown, LockOpen, TrashBin} from '@gravity-ui/icons'; import type {DropdownMenuItem} from '@gravity-ui/uikit'; import {Button, DropdownMenu, Icon, Tooltip} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import { DIALOG_COPY_WORKBOOK, DIALOG_DELETE_WORKBOOK, + DIALOG_EXPORT_WORKBOOK, DIALOG_MOVE_WORKBOOK, } from 'components/CollectionsStructure'; import {I18N} from 'i18n'; @@ -124,6 +125,25 @@ export const WorkbookActions: React.FC = ({workbook, refreshWorkbookInfo} }); } + dropdownActions.push({ + action: () => { + dispatch( + openDialog({ + id: DIALOG_EXPORT_WORKBOOK, + props: { + open: true, + workbookId: workbook.workbookId, + workbookTitle: workbook.title, + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); + }, + text: , + }); + const otherActions: DropdownMenuItem[] = []; if (workbook.permissions.delete) {