diff --git a/README.md b/README.md index 1a20fa50cf..3a7c467c24 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,15 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### CloudBeaver 1.0.2 - 2020-05-19 + +- Data export feature UI was improved +- User authentication and permission management +- Driver management was redesigned +- Clickhouse driver was added +- Build procedure was redesigned +- Many minor bugfixes + ### CloudBeaver 1.0.1 - 2020-05-05 - Data export feature added to the data viewer diff --git a/deploy/build.bat b/deploy/build.bat index 8d176146f5..c47b1e1009 100644 --- a/deploy/build.bat +++ b/deploy/build.bat @@ -47,3 +47,5 @@ echo Copy static content xcopy /E /Q ..\webapp\packages\dbeaver\dist cloudbeaver\web >NUL echo Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server. + +pause \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java index c8dc2ed9df..78b26a2b20 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java @@ -30,16 +30,15 @@ import org.eclipse.core.runtime.Status; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.meta.Association; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNModel; +import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.navigator.DBNProject; import org.jkiss.dbeaver.model.navigator.DBNProjectDatabases; import org.jkiss.dbeaver.model.runtime.*; -import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; @@ -171,8 +170,8 @@ public class WebSession { } private void initNavigatorModel() { - DBPPlatform platform = DBWorkbench.getPlatform(); - this.navigatorModel = new DBNModel(platform, false); + CBPlatform platform = CBPlatform.getInstance(); + this.navigatorModel = new DBNModel(platform, this); this.navigatorModel.initialize(); DBPProject project = platform.getWorkspace().getActiveProject(); @@ -252,6 +251,10 @@ public class WebSession { return databases; } + public DBNNode getNavigatorNodes() { + return databases; + } + /** * Returns and clears progress messages */ diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBResourceHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBResourceHandler.java index ddb9543380..c22b8129ae 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBResourceHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBResourceHandler.java @@ -62,7 +62,7 @@ public class CBResourceHandler implements DBPResourceHandler { @Override public String getResourceNodeName(IResource resource) { - return resource.toString(); + return resource.getName(); } @NotNull diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 853991dffd..ef24a556df 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -19,10 +19,10 @@ package io.cloudbeaver.service.navigator.impl; import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.WebConnectionInfo; -import io.cloudbeaver.service.navigator.WebDatabaseObjectInfo; -import io.cloudbeaver.service.navigator.WebNavigatorNodeInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.navigator.DBWServiceNavigator; +import io.cloudbeaver.service.navigator.WebDatabaseObjectInfo; +import io.cloudbeaver.service.navigator.WebNavigatorNodeInfo; import io.cloudbeaver.service.navigator.WebStructContainers; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; @@ -53,18 +53,28 @@ public class WebServiceNavigator implements DBWServiceNavigator { try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode parentNode = CommonUtils.isEmpty(parentPath) || "/".equals(parentPath) ? session.getDatabases() : session.getNavigatorModel().getNodeByPath(monitor, parentPath); - if (parentNode == null) { - throw new DBWebException("Node '" + parentPath + "' not found"); + DBNNode[] nodeChildren; + if (CommonUtils.isEmpty(parentPath) || "/".equals(parentPath)) { + nodeChildren = session.getDatabases().getChildren(monitor); + } else { + DBNNode parentNode = session.getNavigatorModel().getNodeByPath(monitor, parentPath); + if (parentNode == null) { + throw new DBWebException("Node '" + parentPath + "' not found"); + } + if (!parentNode.hasChildren(true)) { + return EMPTY_NODE_LIST; + } + nodeChildren = parentNode.getChildren(monitor); } - if (!parentNode.hasChildren(true)) { - return EMPTY_NODE_LIST; - } - DBNNode[] nodeChildren = parentNode.getChildren(monitor); if (nodeChildren == null) { return EMPTY_NODE_LIST; } List result = new ArrayList<>(); + // Add navigator extensions + for (DBNNode extraNode : session.getNavigatorModel().getRoot().getExtraNodes()) { + result.add(new WebNavigatorNodeInfo(session, extraNode)); + } + for (DBNNode node : nodeChildren) { if (!CommonUtils.toBoolean(onlyFolders) || node instanceof DBNContainer) { result.add(new WebNavigatorNodeInfo(session, node)); diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index a425211d79..dd775dd70b 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -29,6 +29,8 @@ import java.util.Map; */ public interface DBWServiceAuth extends DBWService { + String ATTR_USER_AUTH = "user-auth-info"; + @WebAction(requirePermissions = {} ) WebAuthInfo authLogin(WebSession webSession, String providerId, Map credentials) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAuthInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAuthInfo.java index 1caf60eafc..d279923927 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAuthInfo.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAuthInfo.java @@ -16,11 +16,10 @@ */ package io.cloudbeaver.service.auth; +import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; -import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.Date; /** * WebAuthInfo @@ -76,4 +75,8 @@ public class WebAuthInfo { public void setMessage(String message) { this.message = message; } + + public static WebAuthInfo getFromSession(WebSession webSession) { + return webSession.getAttribute(DBWServiceAuth.ATTR_USER_AUTH); + } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 6f9d1cbb2f..fc54543a5f 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -42,8 +42,6 @@ import java.util.Map; */ public class WebServiceAuthImpl implements DBWServiceAuth { - private static final String ATTR_USER_AUTH = "user-auth-info"; - @Override public WebAuthInfo authLogin(WebSession webSession, String providerId, Map authParameters) throws DBWebException { if (!CBApplication.getInstance().getAppConfiguration().isAuthenticationEnabled()) { diff --git a/server/product/web-server/CloudbeaverServer.product.launch b/server/product/web-server/CloudbeaverServer.product.launch index 954d8d4a80..7b4e4a0f78 100644 --- a/server/product/web-server/CloudbeaverServer.product.launch +++ b/server/product/web-server/CloudbeaverServer.product.launch @@ -34,8 +34,8 @@ - - + + diff --git a/webapp/packages/core/assets/locales/en.js b/webapp/packages/core/assets/locales/en.js index fcdb744f45..740bb9c556 100644 --- a/webapp/packages/core/assets/locales/en.js +++ b/webapp/packages/core/assets/locales/en.js @@ -76,7 +76,7 @@ module.exports = { app_shared_settingsMenu_theme: 'Theme', app_shared_settingsMenu_lang: 'Language', - core_eventsLog_dbeaverErrorDetails: 'DBeaver error details', + core_eventsLog_dbeaverErrorDetails: 'Error details', app_shared_inlineEditor_dialog_title: 'Edit mode', app_shared_inlineEditor_dialog_apply: 'Apply', diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts index 738b97f439..aa760a237f 100644 --- a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts @@ -56,7 +56,7 @@ export class ConnectionDialogsService { id: 'closeConnection', isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType - && Boolean((context.data as NodeWithParent)?.object?.features?.includes(EObjectFeature.dataSource)); + && !!context.data.object?.features?.includes(EObjectFeature.dataSource); }, title: 'Disconnect', onClick: (context: IMenuContext) => { diff --git a/webapp/packages/core/src/app/shared/NodesManager/NodeManagerUtils.ts b/webapp/packages/core/src/app/shared/NodesManager/NodeManagerUtils.ts index 1546f26298..5f142cca54 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/NodeManagerUtils.ts +++ b/webapp/packages/core/src/app/shared/NodesManager/NodeManagerUtils.ts @@ -17,4 +17,13 @@ export class NodeManagerUtils { return connectionNodeId.replace('database://', ''); } + static nodeIdToConnectionId(nodeId: string): string { + const matches = nodeId.match(/database:\/\/(.*?)(|\/.*)$/); + if (!matches) { + throw new Error('Not database object'); + } + + return matches[1]; + } + } diff --git a/webapp/packages/core/src/dialogs/Menu/ContextMenu/IContextMenuItem.ts b/webapp/packages/core/src/dialogs/Menu/ContextMenu/IContextMenuItem.ts index e2dddb9e9a..7748bf62a9 100644 --- a/webapp/packages/core/src/dialogs/Menu/ContextMenu/IContextMenuItem.ts +++ b/webapp/packages/core/src/dialogs/Menu/ContextMenu/IContextMenuItem.ts @@ -15,7 +15,7 @@ import { IMenuContext } from './IMenuContext'; export interface IContextMenuItem extends IMenuItemOptions { onClick?: (context: IMenuContext) => void; // if isPresent is false menu item will not be included in resulting context menu - isPresent: (context: IMenuContext) => boolean; + isPresent: (context: IMenuContext) => boolean; isDisabled?: (context: IMenuContext) => boolean; // When the item is present in menu it can be hidden based on certain conditions isHidden?: (context: IMenuContext) => boolean; diff --git a/webapp/packages/data-export-plugin/src/DataExportMenuService.ts b/webapp/packages/data-export-plugin/src/DataExportMenuService.ts index e975c7cfd5..ae6885370d 100644 --- a/webapp/packages/data-export-plugin/src/DataExportMenuService.ts +++ b/webapp/packages/data-export-plugin/src/DataExportMenuService.ts @@ -6,8 +6,13 @@ * you may not use this file except in compliance with the License. */ +import { + NavigationTreeContextMenuService, NodeManagerUtils, NodeWithParent, EObjectFeature +} from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; -import { IContextMenuItem, IMenuContext, CommonDialogService } from '@dbeaver/core/dialogs'; +import { + IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService +} from '@dbeaver/core/dialogs'; import { TableFooterMenuService, TableViewerModel } from '@dbeaver/data-viewer-plugin'; import { DataExportDialog } from './Dialog/DataExportDialog'; @@ -18,6 +23,7 @@ export class DataExportMenuService { constructor( private commonDialogService: CommonDialogService, private tableFooterMenuService: TableFooterMenuService, + private contextMenuService: ContextMenuService, ) { } register() { @@ -32,6 +38,27 @@ export class DataExportMenuService { onClick: this.exportData.bind(this), }; this.tableFooterMenuService.registerMenuItem(exportData); + + this.contextMenuService.addMenuItem( + this.contextMenuService.getRootMenuToken(), + { + id: 'export', + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType + && !!context.data.object?.features?.includes(EObjectFeature.dataContainer); + }, + order: 2, + title: 'Export', + onClick: (context) => { + const node = context.data; + const connectionId = NodeManagerUtils.nodeIdToConnectionId(node.id); + this.commonDialogService.open(DataExportDialog, { + connectionId, + containerNodePath: node.id, + }); + }, + } + ); } private exportData(context: IMenuContext) { diff --git a/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts b/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts index 9773dabce2..a07e1817dc 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts +++ b/webapp/packages/data-export-plugin/src/Dialog/DataExportController.ts @@ -8,10 +8,12 @@ import { observable, computed } from 'mobx'; +import { ErrorDetailsDialog } from '@dbeaver/core/app'; import { IProperty } from '@dbeaver/core/blocks'; -import { injectable, IInitializableController } from '@dbeaver/core/di'; +import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; import { NotificationService } from '@dbeaver/core/eventsLog'; -import { DataTransferProcessorInfo } from '@dbeaver/core/sdk'; +import { DataTransferProcessorInfo, GQLErrorCatcher } from '@dbeaver/core/sdk'; import { DataExportService } from '../DataExportService'; import { IExportContext } from '../IExportContext'; @@ -22,7 +24,7 @@ export enum DataExportStep { } @injectable() -export class DataExportController implements IInitializableController { +export class DataExportController implements IInitializableController, IDestructibleController { @observable step = DataExportStep.DataTransferProcessor get isLoading() { return this.dataExportService.processors.isLoading(); @@ -35,18 +37,22 @@ export class DataExportController implements IInitializableController { .from( this.dataExportService.processors.data.values() ) - .sort((a, b) => this.sortProcessors(a, b)); + .sort(sortProcessors); } @observable processorProperties: any = {} @observable properties: IProperty[] = [] + readonly error = new GQLErrorCatcher(); + private context!: IExportContext; private close!: () => void; + private isDistructed = false; constructor( private dataExportService: DataExportService, - private notificationService: NotificationService + private notificationService: NotificationService, + private commonDialogService: CommonDialogService ) { } init(context: IExportContext, close: () => void) { @@ -55,6 +61,10 @@ export class DataExportController implements IInitializableController { this.loadProcessors(); } + destruct(): void { + this.isDistructed = true; + } + prepareExport = async () => { if (!this.processor || this.isExporting) { return; @@ -71,7 +81,9 @@ export class DataExportController implements IInitializableController { ); this.close(); } catch (exception) { - this.notificationService.logException(exception, 'Can\'t export'); + if (!this.error.catch(exception) || this.isDistructed) { + this.notificationService.logException(exception, 'Can\'t export'); + } } finally { this.isExporting = false; close(); @@ -100,6 +112,13 @@ export class DataExportController implements IInitializableController { this.processorProperties = {}; this.step = DataExportStep.Configure; + this.error.clear(); + } + + showDetails = () => { + if (this.error.exception) { + this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); + } } private async loadProcessors() { @@ -109,13 +128,13 @@ export class DataExportController implements IInitializableController { this.notificationService.logException(exception, 'Can\'t load data export processors'); } } +} - private sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number { - if (processorA.order === processorB.order) - { - return (processorA.name || '').localeCompare((processorB.name || '')); - } +function sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number { + if (processorA.order === processorB.order) + { + return (processorA.name || '').localeCompare((processorB.name || '')); + } - return processorA.order - processorB.order; - } + return processorA.order - processorB.order; } diff --git a/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx b/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx index dbb0db6e68..fa1acd83bd 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx +++ b/webapp/packages/data-export-plugin/src/Dialog/DataExportDialog.tsx @@ -26,7 +26,9 @@ export const DataExportDialog: DialogComponent = observer( processor={controller.processor} properties={controller.properties} processorProperties={controller.processorProperties} + error={controller.error} isExporting={controller.isExporting} + onShowDetails={controller.showDetails} onBack={() => controller.setStep(DataExportStep.DataTransferProcessor)} onClose={props.rejectDialog} onExport={controller.prepareExport} diff --git a/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx b/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx index 847d35efd2..d150fdbd5a 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx +++ b/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx @@ -9,31 +9,49 @@ import { observer } from 'mobx-react'; import styled, { css } from 'reshadow'; -import { IProperty, PropertiesTable } from '@dbeaver/core/blocks'; +import { IProperty, PropertiesTable, ErrorMessage } from '@dbeaver/core/blocks'; import { CommonDialogWrapper } from '@dbeaver/core/dialogs'; import { useTranslate } from '@dbeaver/core/localization'; -import { DataTransferProcessorInfo } from '@dbeaver/core/sdk'; +import { DataTransferProcessorInfo, GQLErrorCatcher } from '@dbeaver/core/sdk'; +import { composes, useStyles } from '@dbeaver/core/theming'; import { ProcessorConfigureDialogFooter } from './ProcessorConfigureDialogFooter'; -const styles = css` - CommonDialogWrapper { - max-height: 500px; - min-height: 500px; - } - PropertiesTable { - flex: 1; - } - message { - margin: auto; - } -`; +const styles = composes( + css` + Tab { + composes: theme-ripple theme-background-secondary theme-text-on-secondary from global; + } + ErrorMessage { + composes: theme-background-secondary from global; + } + `, + css` + CommonDialogWrapper { + max-height: 500px; + min-height: 500px; + } + PropertiesTable { + flex: 1; + } + message { + margin: auto; + } + ErrorMessage { + position: sticky; + bottom: 0; + padding: 8px 24px; + } + ` +); type ProcessorSelectDialogProps = { processor: DataTransferProcessorInfo; properties: IProperty[]; processorProperties: any; + error: GQLErrorCatcher; isExporting: boolean; + onShowDetails(): void; onClose(): void; onBack(): void; onExport(): void; @@ -44,7 +62,9 @@ export const ProcessorConfigureDialog = observer( processor, properties, processorProperties, + error, isExporting, + onShowDetails, onClose, onBack, onExport, @@ -52,7 +72,7 @@ export const ProcessorConfigureDialog = observer( const translate = useTranslate(); const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`; - return styled(styles)( + return styled(useStyles(styles))( - {isExporting && {translate('data_transfer_dialog_preparation')}} - {!isExporting && ( - + {error.responseMessage && ( + )} diff --git a/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx b/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx index b5e7826250..ad1c1a59f3 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx +++ b/webapp/packages/data-export-plugin/src/Dialog/ProcessorSelectDialog.tsx @@ -66,7 +66,7 @@ export const ProcessorSelectDialog = observer( > {context.sourceName ? translate('data_transfer_exporting_sql') : `${translate('data_transfer_exporting_table')} ${node?.name}`} -
{context.sourceName}
+
{context.sourceName}
{isLoading && } {!isLoading && } diff --git a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx index adf58fa64f..04faf81739 100644 --- a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx +++ b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotification.tsx @@ -59,7 +59,7 @@ export const ExportNotification = observer(function ExportNotification({ {controller.sourceName} -
{controller.task?.context.sourceName}
+
{controller.task?.context.sourceName}
{controller.isSuccess && ( diff --git a/webapp/packages/dbeaver/README.md b/webapp/packages/dbeaver/README.md deleted file mode 100644 index 9bf004ef40..0000000000 --- a/webapp/packages/dbeaver/README.md +++ /dev/null @@ -1 +0,0 @@ -See [Wiki](https://github.com/dbeaver-corp/dbeaver-web/wiki/Frontend-CI&CD)