mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
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:
co-authored by
naumov
kseniaguzeeva
parent
3274988d4a
commit
bc7cd3b217
+1
@@ -19,6 +19,7 @@ input DataTransferOutputSettingsInput {
|
||||
encoding: String
|
||||
timestampPattern: String
|
||||
compress: Boolean
|
||||
fileName: String
|
||||
}
|
||||
|
||||
type DataTransferOutputSettings {
|
||||
|
||||
+2
-1
@@ -31,7 +31,8 @@ public class WebDataTransferDefaultExportSettings {
|
||||
false,
|
||||
defConsumerSettings.getOutputEncoding(),
|
||||
defConsumerSettings.getOutputTimestampPattern(),
|
||||
defConsumerSettings.isCompressResults()
|
||||
defConsumerSettings.isCompressResults(),
|
||||
null
|
||||
);
|
||||
this.supportedEncodings = Charset.availableCharsets().keySet();
|
||||
}
|
||||
|
||||
+8
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -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`);
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user