CB-4289 add file name param for data export (#2308)

* CB-4289 add file name param for data export

* CB-4289 pass file name based on context

* CB-4289 use existing export action

---------

Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
This commit is contained in:
Ainur
2024-01-22 18:30:36 +03:00
committed by GitHub
co-authored by naumov kseniaguzeeva
parent 3274988d4a
commit bc7cd3b217
18 changed files with 140 additions and 87 deletions
@@ -19,6 +19,7 @@ input DataTransferOutputSettingsInput {
encoding: String
timestampPattern: String
compress: Boolean
fileName: String
}
type DataTransferOutputSettings {
@@ -31,7 +31,8 @@ public class WebDataTransferDefaultExportSettings {
false,
defConsumerSettings.getOutputEncoding(),
defConsumerSettings.getOutputTimestampPattern(),
defConsumerSettings.isCompressResults()
defConsumerSettings.isCompressResults(),
null
);
this.supportedEncodings = Charset.availableCharsets().keySet();
}
@@ -25,19 +25,22 @@ public class WebDataTransferOutputSettings {
private final String encoding;
private final String timestampPattern;
private final boolean compress;
private final String fileName;
public WebDataTransferOutputSettings(Map<String, Object> outputSettings) {
this.insertBom = JSONUtils.getBoolean(outputSettings, "insertBom", false);
this.encoding = JSONUtils.getString(outputSettings, "encoding");
this.timestampPattern = JSONUtils.getString(outputSettings, "timestampPattern");
this.compress = JSONUtils.getBoolean(outputSettings, "compress", false);
this.fileName = JSONUtils.getString(outputSettings, "fileName");
}
public WebDataTransferOutputSettings(boolean insertBom, String encoding, String timestampPattern, boolean compress) {
public WebDataTransferOutputSettings(boolean insertBom, String encoding, String timestampPattern, boolean compress, String fileName) {
this.insertBom = insertBom;
this.encoding = encoding;
this.timestampPattern = timestampPattern;
this.compress = compress;
this.fileName = fileName;
}
public boolean isInsertBom() {
@@ -55,4 +58,8 @@ public class WebDataTransferOutputSettings {
public boolean isCompress() {
return compress;
}
public String getFileName() {
return fileName;
}
}
@@ -173,12 +173,15 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
}
throw new DBException("Error exporting data", e);
}
Path finallyExportFile = parameters.getOutputSettings().isCompress()
var outputSettings = parameters.getOutputSettings();
Path finallyExportFile = outputSettings.isCompress()
? exportFile.resolveSibling(WebDataTransferUtils.normalizeFileName(
exportFile.getFileName().toString(), parameters.getOutputSettings()))
exportFile.getFileName().toString(), outputSettings))
: exportFile;
WebDataTransferTaskConfig taskConfig = new WebDataTransferTaskConfig(finallyExportFile, parameters);
String exportFileName = CommonUtils.escapeFileName(CommonUtils.truncateString(dataContainer.getName(), 32));
String exportFileName = CommonUtils.isEmpty(outputSettings.getFileName()) ?
CommonUtils.escapeFileName(CommonUtils.truncateString(dataContainer.getName(), 32)) :
outputSettings.getFileName();
taskConfig.setExportFileName(exportFileName);
WebDataTransferUtils.getSessionDataTransferConfig(sqlProcessor.getWebSession()).addTask(taskConfig);
@@ -110,6 +110,7 @@ export default [
['ui_disable', 'Disable'],
['ui_readonly', 'Read-only'],
['ui_test', 'Test'],
['ui_export', 'Export'],
['root_permission_denied', "You don't have permissions"],
['root_permission_no_permission', "You don't have permission for this action"],
@@ -94,6 +94,7 @@ export default [
['ui_disable', 'Disable'],
['ui_readonly', 'In sola lettura'],
['ui_test', 'Test'],
['ui_export', 'Export'],
['root_permission_denied', 'Non hai i permessi'],
['app_root_session_expire_warning_title', 'La sessione sta per scadere'],
@@ -106,6 +106,7 @@ export default [
['ui_disable', 'Отключить'],
['ui_readonly', 'Доступно только для чтения'],
['ui_test', 'Проверить'],
['ui_export', 'Экспорт'],
['root_permission_denied', 'Отказано в доступе'],
['root_permission_no_permission', 'У вас нет разрешения на это действие'],
@@ -107,6 +107,7 @@ export default [
['ui_disable', 'Disable'],
['ui_readonly', '只读'],
['ui_test', 'Test'],
['ui_export', 'Export'],
['root_permission_denied', '您没有权限'],
['root_permission_no_permission', '您没有权限执行此操作'],
@@ -1,24 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { generateFileName } from './generateFileName';
describe('generateFileName', () => {
it('should generate a file name in the expected format', () => {
const mockDate = new Date('2020-09-09T14:13:20');
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const fileName = 'my-file';
const fileFormat = '.txt';
const expectedFileName = `${fileName} 2020-09-09 14-13-20${fileFormat}`;
// Test the generateFileName function
expect(generateFileName(fileName, fileFormat)).toEqual(expectedFileName);
spy.mockRestore();
});
});
+1 -1
View File
@@ -20,7 +20,6 @@ export * from './copyToClipboard';
export * from './declensionOfNumber';
export * from './isNotNullDefined';
export * from './flat';
export * from './generateFileName';
export * from './getMIME';
export * from './getOS';
export * from './getPathName';
@@ -72,3 +71,4 @@ export * from './renamePathName';
export * from './removeLineBreak';
export * from './replaceSubstring';
export * from './formatNumber';
export * from './withTimestamp';
@@ -0,0 +1,22 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { withTimestamp } from './withTimestamp';
describe('withTimestamp', () => {
it('should generate a value with timestamp at the end', () => {
const mockDate = new Date('2020-09-09T14:13:20');
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const value = 'value';
const expectedValue = `${value} 2020-09-09 14-13-20`;
expect(withTimestamp(value)).toEqual(expectedValue);
spy.mockRestore();
});
});
@@ -6,9 +6,9 @@
* you may not use this file except in compliance with the License.
*/
export function generateFileName(fileName: string, fileFormat: string) {
export function withTimestamp(value: string) {
const now = new Date();
return `${fileName} ${now.toISOString().slice(0, 10)} ${('0' + now.getHours()).slice(-2)}-${('0' + now.getMinutes()).slice(-2)}-${(
return `${value} ${now.toISOString().slice(0, 10)} ${('0' + now.getHours()).slice(-2)}-${('0' + now.getMinutes()).slice(-2)}-${(
'0' + now.getSeconds()
).slice(-2)}${fileFormat}`;
).slice(-2)}`;
}
@@ -8,5 +8,6 @@
import { createAction } from '../createAction';
export const ACTION_EXPORT = createAction('export', {
label: 'data_transfer_dialog_export',
label: 'ui_export',
tooltip: 'ui_export',
});
@@ -8,11 +8,19 @@
import { EAdminPermission } from '@cloudbeaver/core-authentication';
import { createConnectionParam, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, IMenuContext } from '@cloudbeaver/core-dialogs';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { DATA_CONTEXT_NAV_NODE, EObjectFeature } from '@cloudbeaver/core-navigation-tree';
import { SessionPermissionsResource } from '@cloudbeaver/core-root';
import { ACTION_EXPORT, ActionService, DATA_CONTEXT_MENU_NESTED, MenuService } from '@cloudbeaver/core-view';
import { IDatabaseDataSource, IDataContainerOptions, ITableFooterMenuContext, TableFooterMenuService } from '@cloudbeaver/plugin-data-viewer';
import { withTimestamp } from '@cloudbeaver/core-utils';
import { ACTION_EXPORT, ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_MENU_NESTED, menuExtractItems, MenuService } from '@cloudbeaver/core-view';
import {
DATA_CONTEXT_DV_DDM,
DATA_CONTEXT_DV_DDM_RESULT_INDEX,
DATA_VIEWER_DATA_MODEL_ACTIONS_MENU,
IDatabaseDataSource,
IDataContainerOptions,
} from '@cloudbeaver/plugin-data-viewer';
import type { IDataQueryOptions } from '@cloudbeaver/plugin-sql-editor';
import { DataExportSettingsService } from './DataExportSettingsService';
@@ -22,32 +30,83 @@ import { DataExportDialog } from './Dialog/DataExportDialog';
export class DataExportMenuService {
constructor(
private readonly commonDialogService: CommonDialogService,
private readonly tableFooterMenuService: TableFooterMenuService,
private readonly dataExportSettingsService: DataExportSettingsService,
private readonly actionService: ActionService,
private readonly menuService: MenuService,
private readonly sessionPermissionsResource: SessionPermissionsResource,
private readonly localizationService: LocalizationService,
) {}
register(): void {
this.tableFooterMenuService.registerMenuItem({
id: 'export ',
order: 5,
title: 'data_transfer_dialog_export',
tooltip: 'data_transfer_dialog_export_tooltip',
icon: 'table-export',
isPresent(context) {
return context.contextType === TableFooterMenuService.nodeContextType;
this.actionService.addHandler({
id: 'data-export-base-handler',
isActionApplicable(context, action) {
const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_ACTIONS_MENU);
const model = context.tryGet(DATA_CONTEXT_DV_DDM);
const resultIndex = context.tryGet(DATA_CONTEXT_DV_DDM_RESULT_INDEX);
if (!menu || !model || resultIndex === undefined) {
return false;
}
return [ACTION_EXPORT].includes(action);
},
isHidden: () => this.isDisabled(),
isDisabled(context) {
return (
context.data.model.isLoading() ||
context.data.model.isDisabled(context.data.resultIndex) ||
!context.data.model.getResult(context.data.resultIndex)
);
const model = context.get(DATA_CONTEXT_DV_DDM);
const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX);
return model.isLoading() || model.isDisabled(resultIndex) || !model.getResult(resultIndex);
},
getActionInfo(context, action) {
if (action === ACTION_EXPORT) {
return { ...action.info, icon: 'table-export' };
}
return action.info;
},
handler: (context, action) => {
const model = context.get(DATA_CONTEXT_DV_DDM);
const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX);
if (action === ACTION_EXPORT) {
const result = model.getResult(resultIndex);
if (!result) {
throw new Error('Result must be provided');
}
const source = model.source as IDatabaseDataSource<IDataContainerOptions & IDataQueryOptions>;
if (!source.options) {
throw new Error('Source options must be provided');
}
this.commonDialogService.open(DataExportDialog, {
connectionKey: source.options.connectionKey,
contextId: source.executionContext?.context?.id,
containerNodePath: source.options.containerNodePath,
resultId: result.id,
name: model.name ?? undefined,
fileName: withTimestamp(model.name ?? this.localizationService.translate('data_transfer_dialog_title')),
query: source.options.query,
filter: {
constraints: source.options.constraints,
where: source.options.whereFilter,
},
});
}
},
});
this.menuService.addCreator({
menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU],
isApplicable: () => !this.isExportDisabled(),
getItems(context, items) {
return [...items, ACTION_EXPORT];
},
orderItems(context, items) {
const extracted = menuExtractItems(items, [ACTION_EXPORT]);
return [...items, ...extracted];
},
onClick: this.exportData.bind(this),
});
this.menuService.addCreator({
@@ -58,7 +117,7 @@ export class DataExportMenuService {
return false;
}
return !this.isDisabled() && context.has(DATA_CONTEXT_CONNECTION) && !context.has(DATA_CONTEXT_MENU_NESTED);
return !this.isExportDisabled() && context.has(DATA_CONTEXT_CONNECTION) && !context.has(DATA_CONTEXT_MENU_NESTED);
},
getItems: (context, items) => [...items, ACTION_EXPORT],
});
@@ -69,44 +128,19 @@ export class DataExportMenuService {
handler: async (context, action) => {
const node = context.get(DATA_CONTEXT_NAV_NODE);
const connection = context.get(DATA_CONTEXT_CONNECTION);
const fileName = withTimestamp(`${connection.name}${node?.name ? ` - ${node.name}` : ''}`);
this.commonDialogService.open(DataExportDialog, {
connectionKey: createConnectionParam(connection),
name: node?.name,
fileName,
containerNodePath: node?.id,
});
},
});
}
private exportData(context: IMenuContext<ITableFooterMenuContext>) {
const result = context.data.model.getResult(context.data.resultIndex);
if (!result) {
throw new Error('Result must be provided');
}
const source = context.data.model.source as IDatabaseDataSource<IDataContainerOptions & IDataQueryOptions>;
if (!source.options) {
throw new Error('Source options must be provided');
}
this.commonDialogService.open(DataExportDialog, {
connectionKey: source.options.connectionKey,
contextId: context.data.model.source.executionContext?.context?.id,
containerNodePath: source.options.containerNodePath,
resultId: result.id,
name: context.data.model.name ?? undefined,
query: source.options.query,
filter: {
constraints: source.options.constraints,
where: source.options.whereFilter,
},
});
}
private isDisabled() {
private isExportDisabled() {
if (this.sessionPermissionsResource.has(EAdminPermission.admin)) {
return false;
}
@@ -94,7 +94,10 @@ export function useDataExportDialog(context: IExportContext, onExport?: () => vo
processorId: this.processor.id,
processorProperties: this.processorProperties,
filter: this.context.filter,
outputSettings: this.outputSettings,
outputSettings: {
...this.outputSettings,
fileName: this.context.fileName,
},
});
this.onExport?.();
@@ -14,6 +14,7 @@ export interface IExportContext {
resultId?: string | null;
containerNodePath?: string;
name?: string;
fileName?: string;
query?: string;
filter?: SqlDataFilter;
}
@@ -8,7 +8,7 @@
import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { download, generateFileName } from '@cloudbeaver/core-utils';
import { download, withTimestamp } from '@cloudbeaver/core-utils';
import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view';
import { LocalStorageSqlDataSource } from '@cloudbeaver/plugin-sql-editor';
import { ACTION_SQL_EDITOR_OPEN, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
@@ -58,7 +58,7 @@ export class DDLViewerFooterService {
const node = nodeId ? this.navNodeManagerService.getNode(nodeId) : undefined;
const name = node?.name ? `DDL_${node.nodeType ? node.nodeType + '_' : ''}${node.name}` : 'DDL';
download(blob, generateFileName(name, '.sql'));
download(blob, `${withTimestamp(name)}.sql`);
break;
}
case ACTION_SQL_EDITOR_OPEN: {
@@ -12,7 +12,7 @@ import { Connection, ConnectionInfoResource, createConnectionParam } from '@clou
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { download, generateFileName, getTextFileReadingProcess } from '@cloudbeaver/core-utils';
import { download, getTextFileReadingProcess, withTimestamp } from '@cloudbeaver/core-utils';
import { getSqlEditorName } from '../getSqlEditorName';
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
@@ -110,7 +110,7 @@ export function useTools(state: ISqlEditorTabState): Readonly<State> {
const name = getSqlEditorName(this.state, dataSource, connection);
download(blob, generateFileName(name, '.sql'));
download(blob, `${withTimestamp(name)}.sql`);
},
}),
{