diff --git a/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql b/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql new file mode 100644 index 0000000000..ba9a4ad4c7 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql @@ -0,0 +1,9 @@ +mutation getResultsetDataURL($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $lobColumnIndex: ID!, $row: [ SQLResultRow! ]) { + url: readLobValue( + connectionId: $connectionId, + contextId: $contextId, + resultsId: $resultsId, + lobColumnIndex: $lobColumnIndex, + row: $row + ) +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index b868bd1005..51fb5e282f 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -225,6 +225,7 @@ export interface DatabaseAuthModel { icon?: Maybe; id: Scalars['ID']; properties: Array; + requiresLocalConfiguration?: Maybe; } export interface DatabaseCatalog { @@ -328,6 +329,7 @@ export interface Mutation { /** @deprecated Field no longer supported */ openConnection: ConnectionInfo; openSession: SessionInfo; + readLobValue: Scalars['String']; refreshSessionConnections?: Maybe; setConnectionNavigatorSettings: ConnectionInfo; setUserConfigurationParameter: Scalars['Boolean']; @@ -459,6 +461,15 @@ export interface MutationOpenSessionArgs { } +export interface MutationReadLobValueArgs { + connectionId: Scalars['ID']; + contextId: Scalars['ID']; + lobColumnIndex: Scalars['ID']; + resultsId: Scalars['ID']; + row?: InputMaybe>; +} + + export interface MutationSetConnectionNavigatorSettingsArgs { id: Scalars['ID']; settings: NavigatorSettingsInput; @@ -1980,6 +1991,17 @@ export type CloseResultMutationVariables = Exact<{ export type CloseResultMutation = { result: boolean }; +export type GetResultsetDataUrlMutationVariables = Exact<{ + connectionId: Scalars['ID']; + contextId: Scalars['ID']; + resultsId: Scalars['ID']; + lobColumnIndex: Scalars['ID']; + row?: InputMaybe | SqlResultRow>; +}>; + + +export type GetResultsetDataUrlMutation = { url: string }; + export type GetSqlExecuteTaskResultsMutationVariables = Exact<{ taskId: Scalars['ID']; }>; @@ -3163,6 +3185,17 @@ export const CloseResultDocument = ` ) } `; +export const GetResultsetDataUrlDocument = ` + mutation getResultsetDataURL($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $lobColumnIndex: ID!, $row: [SQLResultRow!]) { + url: readLobValue( + connectionId: $connectionId + contextId: $contextId + resultsId: $resultsId + lobColumnIndex: $lobColumnIndex + row: $row + ) +} + `; export const GetSqlExecuteTaskResultsDocument = ` mutation getSqlExecuteTaskResults($taskId: ID!) { result: asyncSqlExecuteResults(taskId: $taskId) { @@ -3742,6 +3775,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = closeResult(variables: CloseResultMutationVariables, requestHeaders?: Dom.RequestInit['headers']): Promise { return withWrapper(wrappedRequestHeaders => client.request(CloseResultDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }), 'closeResult', 'mutation'); }, + getResultsetDataURL(variables: GetResultsetDataUrlMutationVariables, requestHeaders?: Dom.RequestInit['headers']): Promise { + return withWrapper(wrappedRequestHeaders => client.request(GetResultsetDataUrlDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }), 'getResultsetDataURL', 'mutation'); + }, getSqlExecuteTaskResults(variables: GetSqlExecuteTaskResultsMutationVariables, requestHeaders?: Dom.RequestInit['headers']): Promise { return withWrapper(wrappedRequestHeaders => client.request(GetSqlExecuteTaskResultsDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }), 'getSqlExecuteTaskResults', 'mutation'); }, diff --git a/webapp/packages/core-utils/src/download.ts b/webapp/packages/core-utils/src/download.ts index efa34a6b4f..0cd0dee02c 100644 --- a/webapp/packages/core-utils/src/download.ts +++ b/webapp/packages/core-utils/src/download.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -export function download(blob: Blob, fileName: string): void { +export function download(content: Blob | string, fileName = ''): void { const saveLink = document.createElement('a'); saveLink.tabIndex = -1; @@ -15,7 +15,7 @@ export function download(blob: Blob, fileName: string): void { document.body.appendChild(saveLink); try { - const url = URL.createObjectURL(blob); + const url = typeof content === 'string' ? content : URL.createObjectURL(content); saveLink.href = url; saveLink.onclick = () => requestAnimationFrame(() => URL.revokeObjectURL(url)); } catch (e: any) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts new file mode 100644 index 0000000000..cbf7bccdee --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts @@ -0,0 +1,58 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@cloudbeaver/core-di'; +import { download } from '@cloudbeaver/core-utils'; +import { DataViewerContentSaverService } from '@cloudbeaver/plugin-data-viewer'; + +import { DataGridContextMenuService } from './DataGridContextMenuService'; + +@injectable() +export class DataGridContextMenuSaveContentService { + private static readonly menuContentSaveToken = 'menuContentSave'; + + constructor( + private readonly dataGridContextMenuService: DataGridContextMenuService, + private readonly dataViewerContentSaverService: DataViewerContentSaverService + ) { } + + getMenuContentSaveToken(): string { + return DataGridContextMenuSaveContentService.menuContentSaveToken; + } + + register(): void { + this.dataGridContextMenuService.add( + this.dataGridContextMenuService.getMenuToken(), + { + id: this.getMenuContentSaveToken(), + order: 4, + title: 'ui_processing_save', + icon: '/icons/save.svg', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + onClick: async context => { + const url = await this.dataViewerContentSaverService.getElementValueURL( + context.data.model, + context.data.resultIndex, + context.data.key + ); + + if (url) { + download(url); + } + }, + isHidden: context => !this.dataViewerContentSaverService.canSaveElementValue( + context.data.model, + context.data.resultIndex, + context.data.key + ), + } + ); + } +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index 3fbaadcd0f..8a43cf048b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -14,6 +14,7 @@ import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService'; import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; +import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService'; import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { SpreadsheetGrid } from './SpreadsheetGrid'; @@ -21,12 +22,13 @@ import { SpreadsheetGrid } from './SpreadsheetGrid'; @injectable() export class SpreadsheetBootstrap extends Bootstrap { constructor( - private dataPresentationService: DataPresentationService, - private dataGridSettingsService: DataGridSettingsService, - private dataGridContextMenuSortingService: DataGridContextMenuOrderService, - private dataGridContextMenuFilterService: DataGridContextMenuFilterService, - private dataGridContextMenuCellEditingService: DataGridContextMenuCellEditingService, - private dataGridContextMenuService: DataGridContextMenuService, + private readonly dataPresentationService: DataPresentationService, + private readonly dataGridSettingsService: DataGridSettingsService, + private readonly dataGridContextMenuSortingService: DataGridContextMenuOrderService, + private readonly dataGridContextMenuFilterService: DataGridContextMenuFilterService, + private readonly dataGridContextMenuCellEditingService: DataGridContextMenuCellEditingService, + private readonly dataGridContextMenuService: DataGridContextMenuService, + private readonly dataGridContextMenuSaveContentService: DataGridContextMenuSaveContentService, exceptionsCatcherService: ExceptionsCatcherService ) { super(); @@ -45,6 +47,7 @@ export class SpreadsheetBootstrap extends Bootstrap { this.dataGridContextMenuSortingService.register(); this.dataGridContextMenuFilterService.register(); this.dataGridContextMenuCellEditingService.register(); + this.dataGridContextMenuSaveContentService.register(); this.dataGridContextMenuService.add( this.dataGridContextMenuService.getMenuToken(), diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts index e244cd6692..0431a12da3 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts @@ -11,6 +11,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService'; import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; +import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService'; import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { LocaleService } from './LocaleService'; @@ -26,5 +27,6 @@ export const manifest: PluginManifest = { DataGridContextMenuOrderService, DataGridContextMenuFilterService, DataGridContextMenuCellEditingService, + DataGridContextMenuSaveContentService, ], }; diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerContentSaverService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerContentSaverService.ts new file mode 100644 index 0000000000..44ecec8dac --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DataViewerContentSaverService.ts @@ -0,0 +1,64 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@cloudbeaver/core-di'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +import type { IResultSetElementKey } from './DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; +import { isResultSetContentValue } from './DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +import { ResultSetDataAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; +import { ResultSetViewAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; +import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel'; + +const RESULT_VALUE_URL = '/api/sql-result-value'; + +@injectable() +export class DataViewerContentSaverService { + + constructor( + private readonly graphQLService: GraphQLService + ) { } + + canSaveElementValue( + model: IDatabaseDataModel, + resultIndex: number, + element: IResultSetElementKey + ) { + const view = model.source.getAction(resultIndex, ResultSetViewAction); + const cellValue = view.getCellValue(element); + + return isResultSetContentValue(cellValue); + } + + async getElementValueURL( + model: IDatabaseDataModel, + resultIndex: number, + element: IResultSetElementKey + ) { + const result = model.getResult(resultIndex); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const column = data.getColumn(element.column); + const row = data.getRowValue(element.row); + + if (!result?.id || !row || !column) { + return; + } + + const response = await this.graphQLService.sdk.getResultsetDataURL({ + resultsId: result.id, + connectionId: result.connectionId, + contextId: result.contextId, + lobColumnIndex: String(column.position), + row: { + data: row, + }, + }); + + return `${RESULT_VALUE_URL}/${response.url}`; + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index c916c7a0b4..e3982fc049 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -38,6 +38,7 @@ export * from './DatabaseDataModel/IDatabaseDataSource'; export * from './DatabaseDataModel/IDatabaseResultSet'; export * from './DatabaseDataModel/Order'; export * from './DataViewerService'; +export * from './DataViewerContentSaverService'; // All Services and Components that is provided by this plugin should be exported here export * from './TableViewer/TableViewerStorageService'; diff --git a/webapp/packages/plugin-data-viewer/src/manifest.ts b/webapp/packages/plugin-data-viewer/src/manifest.ts index 641fb6fdeb..c06b7471e8 100644 --- a/webapp/packages/plugin-data-viewer/src/manifest.ts +++ b/webapp/packages/plugin-data-viewer/src/manifest.ts @@ -10,6 +10,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { DataPresentationService } from './DataPresentationService'; import { DataViewerBootstrap } from './DataViewerBootstrap'; +import { DataViewerContentSaverService } from './DataViewerContentSaverService'; import { DataViewerDataChangeConfirmationService } from './DataViewerDataChangeConfirmationService'; import { DataViewerService } from './DataViewerService'; import { DataViewerSettingsService } from './DataViewerSettingsService'; @@ -51,5 +52,6 @@ export const manifest: PluginManifest = { DataValuePanelBootstrap, DataViewerSettingsService, DataViewerService, + DataViewerContentSaverService, ], };