diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java index 9f6340e471..53d5c62fdf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java @@ -83,6 +83,9 @@ public interface DBWSecurityController { WebRole[] findRoles(String roleName) throws DBCException; + @NotNull + String[] getRoleSubjects(String roleId) throws DBCException; + void createRole(WebRole role) throws DBCException; void updateRole(WebRole role) throws DBCException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java index d635f5564f..5967e70817 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java @@ -433,6 +433,26 @@ class CBSecurityController implements DBWSecurityController { return readAllRoles(); } + @NotNull + @Override + public String[] getRoleSubjects(String roleId) throws DBCException { + try (Connection dbCon = database.openConnection()) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "SELECT USER_ID FROM CB_USER_ROLE WHERE ROLE_ID")) { + dbStat.setString(1, roleId); + List subjects = new ArrayList<>(); + try (ResultSet dbResult = dbStat.executeQuery()) { + while (dbResult.next()) { + subjects.add(dbResult.getString(1)); + } + } + return subjects.toArray(new String[0]); + } + } catch (SQLException e) { + throw new DBCException("Error while reading role subjects", e); + } + } + @NotNull private WebRole fetchRole(ResultSet dbResult) throws SQLException { WebRole role = new WebRole(dbResult.getString("ROLE_ID")); diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index b620d3c4a7..6e26ec7218 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -36,6 +36,9 @@ type AdminRoleInfo { roleName: String description: String + grantedUsers: [ID!]! + grantedConnections: [AdminConnectionGrantInfo!]! + rolePermissions: [ID]! } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminRoleInfo.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminRoleInfo.java index 1d6257df20..bbb17970ce 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminRoleInfo.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminRoleInfo.java @@ -16,7 +16,11 @@ */ package io.cloudbeaver.service.admin; +import io.cloudbeaver.DBWConnectionGrant; import io.cloudbeaver.model.user.WebRole; +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.meta.Property; import java.util.List; @@ -51,4 +55,15 @@ public class AdminRoleInfo { public void setRolePermissions(List rolePermissions) { this.rolePermissions = rolePermissions; } + + @Property + public DBWConnectionGrant[] getGrantedConnections() throws DBCException { + return CBPlatform.getInstance().getApplication().getSecurityController().getSubjectConnectionAccess(new String[] { getRoleId()} ); + } + + @Property + public String[] getGrantedUsers() throws DBCException { + return CBPlatform.getInstance().getApplication().getSecurityController().getRoleSubjects(getRoleId()); + } + } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx index c6428a4b79..fce9735aac 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useContext } from 'react'; import styled, { css, use } from 'reshadow'; -import { TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; +import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { composes, useStyles } from '@cloudbeaver/core-theming'; @@ -75,12 +75,12 @@ export const NavigationNodeControl = observer(function NavigationNodeCont }) { const context = useContext(TreeNodeContext); const navNodeInfoResource = useService(NavNodeInfoResource); - const outdated = navNodeInfoResource.isOutdated(node.id) && !context?.loading; + const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !context.loading); const connected = node.objectFeatures.includes(EObjectFeature.dataSourceConnected); const onClickHandler = useCallback((event: React.MouseEvent) => { - context?.select(event.ctrlKey || event.metaKey); + context.select(event.ctrlKey || event.metaKey); }, [context]); return styled(useStyles(TREE_NODE_STYLES, styles))( @@ -91,7 +91,7 @@ export const NavigationNodeControl = observer(function NavigationNodeCont {node.name} - + ); diff --git a/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts b/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts index 1b2d7758cb..f5bc6fcad7 100644 --- a/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts +++ b/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts @@ -40,6 +40,13 @@ function sortManageable(connectionInfoResource: ConnectionInfoResource): (nodeA: const connectionA = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeA.id)); const connectionB = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeB.id)); + if (!connectionA || !connectionB) { + if (connectionA === connectionB) { + return 0; + } + return connectionA ? 1 : -1; + } + const nodeAManageable = connectionA?.features.includes(EConnectionFeature.manageable); const nodeBManageable = connectionB?.features.includes(EConnectionFeature.manageable); @@ -71,7 +78,7 @@ export function navigationTreeConnectionGroupFilter( for (const node of nodes) { const connection = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(node.id)); - const manageable = connection?.features.includes(EConnectionFeature.manageable); + const manageable = !!connection?.features.includes(EConnectionFeature.manageable); let nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unsorted; diff --git a/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialog.tsx b/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialog.tsx index 17e400fc10..840668d049 100644 --- a/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialog.tsx +++ b/webapp/packages/core-app/src/shared/SessionExpireDialog/SessionExpiredDialog.tsx @@ -16,10 +16,6 @@ import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; const styles = css` - CommonDialogWrapper { - width: 400px; - min-width: auto; - } controls { display: flex; flex: 1; @@ -42,6 +38,7 @@ export const SessionExpiredDialog: DialogComponent = observer(functi return styled(useStyles(styles))( @@ -54,6 +51,7 @@ export const SessionExpiredDialog: DialogComponent = observer(functi )} + fixedSize noOverflow onReject={rejectDialog} > diff --git a/webapp/packages/core-app/src/shared/SessionExpireWarningDialog/SessionExpireWarningDialog.tsx b/webapp/packages/core-app/src/shared/SessionExpireWarningDialog/SessionExpireWarningDialog.tsx index 2cd4b952c5..6d0b65b5d5 100644 --- a/webapp/packages/core-app/src/shared/SessionExpireWarningDialog/SessionExpireWarningDialog.tsx +++ b/webapp/packages/core-app/src/shared/SessionExpireWarningDialog/SessionExpireWarningDialog.tsx @@ -15,10 +15,6 @@ import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; const styles = css` - CommonDialogWrapper { - width: 400px; - min-width: auto; - } controls { display: flex; flex: 1; @@ -39,6 +35,7 @@ export const SessionExpireWarningDialog: DialogComponent = observer( return styled(useStyles(styles))( @@ -51,6 +48,7 @@ export const SessionExpireWarningDialog: DialogComponent = observer( )} + fixedSize noOverflow onReject={rejectDialog} > diff --git a/webapp/packages/core-blocks/src/DisplayError.tsx b/webapp/packages/core-blocks/src/DisplayError.tsx index 0c3760692d..c398cb16ad 100644 --- a/webapp/packages/core-blocks/src/DisplayError.tsx +++ b/webapp/packages/core-blocks/src/DisplayError.tsx @@ -9,7 +9,8 @@ import type React from 'react'; import styled, { css, use } from 'reshadow'; -import { ENotificationType } from '../../core-events/src'; +import { ENotificationType } from '@cloudbeaver/core-events'; + import { NotificationMark } from './Snackbars/NotificationMark'; const style = css` @@ -17,7 +18,6 @@ const style = css` width: 100%; height: 100%; display: flex; - justify-content: center; overflow: auto; &[|root] { diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts index 8ea6ad5daa..2316f729cc 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts @@ -21,4 +21,4 @@ export interface ITreeNodeContext { open: () => Promise; } -export const TreeNodeContext = createContext(null); +export const TreeNodeContext = createContext(undefined as any); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx index 25de2ac4ab..57792e1f86 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx @@ -32,10 +32,6 @@ export const TreeNodeControl = observer(function TreeNodeControl({ }) { const context = useContext(TreeNodeContext); - if (!context) { - throw new Error('Context not provided'); - } - const handleEnter = (event: React.KeyboardEvent) => { if (EventContext.has(event, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag)) { return; @@ -44,7 +40,7 @@ export const TreeNodeControl = observer(function TreeNodeControl({ EventContext.set(event, EventTreeNodeSelectFlag); switch ((event as unknown as KeyboardEvent).code) { case KEY.ENTER: - context?.select(event.ctrlKey || event.metaKey); + context.select(event.ctrlKey || event.metaKey); break; } return true; diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index d1ac27ad42..84e150a464 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -11,7 +11,7 @@ import { useContext } from 'react'; import styled from 'reshadow'; import { css } from 'reshadow'; -import { Icon, Loader } from '@cloudbeaver/core-blocks'; +import { getComputed, Icon, Loader } from '@cloudbeaver/core-blocks'; import { EventContext } from '@cloudbeaver/core-events'; import { useStateDelay } from '../../useStateDelay'; @@ -39,10 +39,6 @@ export const TreeNodeExpand = observer(function TreeNodeExpand({ }) { const context = useContext(TreeNodeContext); - if (!context) { - throw new Error('Context not provided'); - } - const handleExpand = (event: React.MouseEvent) => { EventContext.set(event, EventTreeNodeExpandFlag); @@ -55,7 +51,7 @@ export const TreeNodeExpand = observer(function TreeNodeExpand({ EventContext.set(event, EventTreeNodeExpandFlag); }; - const loading = useStateDelay(context.loading || context.processing, 300); + const loading = useStateDelay(getComputed(() => context.loading || context.processing), 300); return styled(styles)( diff --git a/webapp/packages/core-blocks/src/getComputed.ts b/webapp/packages/core-blocks/src/getComputed.ts new file mode 100644 index 0000000000..e10bfc6998 --- /dev/null +++ b/webapp/packages/core-blocks/src/getComputed.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { computed } from 'mobx'; + +export function getComputed(action: () => T): T { + return computed(action).get(); +} diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 0b116b11c8..876be9fdd0 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -133,6 +133,7 @@ export * from './Cell'; export * from './ErrorMessage'; export * from './StatusMessage'; export * from './ExceptionMessage'; +export * from './getComputed'; export * from './IconButton'; export * from './IconOrImage'; export * from './StaticImage'; diff --git a/webapp/packages/core-blocks/src/useObjectRef.ts b/webapp/packages/core-blocks/src/useObjectRef.ts index a9f64bea8c..4ae57dbed9 100644 --- a/webapp/packages/core-blocks/src/useObjectRef.ts +++ b/webapp/packages/core-blocks/src/useObjectRef.ts @@ -13,6 +13,9 @@ export function useObjectRef>( update: false, bind?: Array ): T; +export function useObjectRef>( + init: () => T & ThisType +): T; export function useObjectRef, U extends Record>( init: () => (T & ThisType), update: U & ThisType, @@ -37,7 +40,7 @@ export function useObjectRef, U extends Record = observer return styled(useStyles(styles))( = observer(function Da return styled(useStyles(styles))( = function CommonDialogWrapper({ + size = 'medium', + fixedSize, title, subTitle, icon, @@ -41,7 +45,7 @@ export const CommonDialogWrapper: React.FC = function children, }) { return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle))( - +
{icon && } diff --git a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts index f303fb8548..450c2dcb49 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts +++ b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/styles.ts @@ -44,12 +44,43 @@ export const commonDialogBaseStyle = composes( margin: 0; border: none; height: auto; - min-width: 748px; max-height: 100%; border-radius: 0.25rem; padding: 0px; + + &[|size=small] { + min-width: 404px; + min-height: 262px; + max-height: max(100vh - 48px, 262px); + + &[|fixedSize] { + width: 404px; + height: 262px; + } + } + &[|size=medium] { + min-width: 576px; + min-height: 374px; + max-height: max(100vh - 48px, 374px); + + &[|fixedSize] { + width: 576px; + height: 374px; + } + } + &[|size=large] { + min-width: 720px; + min-height: 468px; + max-height: max(100vh - 48px, 468px); + + &[|fixedSize] { + width: 720px; + height: 468px; + } + } } header, dialog-body, footer { + flex-shrink: 0; padding: 24px; &[|no-padding] { @@ -58,6 +89,7 @@ export const commonDialogBaseStyle = composes( } dialog-body { padding-top: 0px; + padding-right: 0px; } footer { padding-top: 0px; @@ -89,41 +121,42 @@ export const commonDialogBaseStyle = composes( } } dialog-body { - position: relative; - box-sizing: border-box; - display: flex; - flex-direction: row; flex: 1; + box-sizing: content-box; + display: flex; max-width: 748px; max-height: 100%; overflow: auto; } dialog-body-overflow-box { position: relative; + flex: 1; box-sizing: border-box; display: flex; - flex-direction: row; - flex: 1; + flex-direction: column; + overflow: auto; + padding-right: 24px; } dialog-body-content { - position: relative; flex: 1; + position: relative; display: flex; flex-direction: column; box-sizing: border-box; - overflow: auto; - padding-bottom: 24px; } dialog-body[|no-padding] + footer { padding-top: 24px; } + dialog-body[|no-padding] dialog-body-overflow-box { + padding-right: 0; + } dialog-body[|no-overflow] dialog-body-content { - padding-bottom: 0; + overflow: auto; } dialog-body-overflow { - position: absolute; - flex-shrink: 0; + position: sticky; bottom: 0; + flex-shrink: 0; width: 100%; height: 24px; pointer-events: none; diff --git a/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx b/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx index 0d00b8909b..fc65568ad2 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx +++ b/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx @@ -50,7 +50,7 @@ export const ConfirmationDialog: DialogComponent = fu const { icon, title, subTitle, bigIcon, viewBox, message, confirmActionText } = payload; return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle, style))( - +
{icon && } @@ -65,7 +65,7 @@ export const ConfirmationDialog: DialogComponent = fu {subTitle && {subTitle}}
- + diff --git a/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx b/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx index a95276c019..d26592187f 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx +++ b/webapp/packages/core-dialogs/src/CommonDialog/DialogsPortal.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { Dialog, DialogBackdrop, @@ -15,6 +15,7 @@ import { } from 'reakit/Dialog'; import styled from 'reshadow'; +import { useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -22,87 +23,85 @@ import { CommonDialogService, DialogInternal } from './CommonDialogService'; import { dialogStyles } from './styles'; export const DialogsPortal = observer(function DialogsPortal() { + const styles = useStyles(dialogStyles); const commonDialogService = useService(CommonDialogService); - const handleReject = useCallback( - (dialog: DialogInternal) => commonDialogService.rejectDialog(dialog.promise), - [commonDialogService] - ); - const handleResolve = useCallback( - (dialog: DialogInternal, result: any) => commonDialogService.resolveDialog(dialog.promise, result), - [commonDialogService] - ); - return ( - <> - {commonDialogService.dialogs.map((dialog, i, arr) => ( - - ))} - + let activeDialog: DialogInternal | undefined; + + if (commonDialogService.dialogs.length > 0) { + activeDialog = commonDialogService.dialogs[commonDialogService.dialogs.length - 1]; + } + + const state = useObjectRef(() => ({ + reject() { + if (this.dialog) { + commonDialogService.rejectDialog(this.dialog.promise); + } + }, + resolve(result: any) { + if (this.dialog) { + commonDialogService.resolveDialog(this.dialog.promise, result); + } + }, + backdropClick(e: React.MouseEvent) { + if (!this.dialog?.options?.persistent && e.currentTarget.isEqualNode(e.target as HTMLElement)) { + this.reject(); + } + }, + }), { + dialog: activeDialog, + }, ['reject', 'resolve', 'backdropClick']); + + return styled(styles)( + + + {commonDialogService.dialogs.map((dialog, i, arr) => ( + + ))} + + ); }); interface NestedDialogType { dialog: DialogInternal; - resolveDialog: (dialog: DialogInternal, result: any) => void; - rejectDialog: (dialog: DialogInternal) => void; + resolveDialog: (result: any) => void; + rejectDialog: () => void; visible: boolean; } -function NestedDialog({ +const NestedDialog: React.FC = function NestedDialog({ dialog, resolveDialog, rejectDialog, visible, -}: NestedDialogType) { - const lastVisibility = useRef(visible); - const dialogState = useDialogState({ visible }); +}) { + const dialogState = useDialogState({ visible: true }); const styles = useStyles(dialogStyles); useEffect(() => { - if (!dialogState.visible - && dialogState.visible !== lastVisibility.current - && !dialog.options?.persistent - ) { - rejectDialog(dialog); - } else { - lastVisibility.current = visible; - dialogState.setVisible(visible); + if (!dialogState.visible && !dialog.options?.persistent) { + rejectDialog(); } }); - const handleReject = useCallback(() => rejectDialog(dialog), [dialog, rejectDialog]); - const handleResolve = useCallback( - (result: any) => resolveDialog(dialog, result), - [dialog, resolveDialog] - ); - const DialogComponent = dialog.component; - const backdropClickHandler = (e: React.MouseEvent) => { - if (!dialog.options?.persistent && e.currentTarget.isEqualNode(e.target as HTMLElement)) { - rejectDialog(dialog); - } - }; - // TODO: place Dialog inside CommonDialogWrapper, so we can pass aria-label return styled(styles)( - <> - - - - - - + + + ); -} +}; diff --git a/webapp/packages/core-dialogs/src/CommonDialog/styles.ts b/webapp/packages/core-dialogs/src/CommonDialog/styles.ts index 489aee7be2..a3139d499b 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/styles.ts +++ b/webapp/packages/core-dialogs/src/CommonDialog/styles.ts @@ -21,15 +21,20 @@ export const dialogStyles = css` left: 0px; z-index: 999; display: flex; + overflow: auto; + } + + inner-box { + display: flex; + margin: auto; + padding: 24px; + flex-direction: column; align-items: center; - justify-content: center; } Dialog { box-sizing: border-box; display: flex; - max-height: calc(100vh); - z-index: 999; outline: none; } `; diff --git a/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx b/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx index 2d7d410b55..b6ea253e0f 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx +++ b/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx @@ -54,6 +54,7 @@ export const ErrorDetailsDialog: DialogComponent = observe return styled(useStyles(styles))( = observer(function AuthConfigurationOptions({ state, }) { @@ -39,6 +42,10 @@ export const AuthConfigurationOptions: TabContainerPanelComponent provider.configurable); const edit = state.mode === 'edit'; @@ -111,15 +118,32 @@ export const AuthConfigurationOptions: TabContainerPanelComponent {parameters.isLoaded() && parameters.data && ( - - {translate('administration_identity_providers_provider_configuration_parameters')} - - + <> + {isUncategorizedExists && ( + + {translate('administration_identity_providers_provider_configuration_parameters')} + + + )} + {categories.map(category => ( + + {category} + + + ))} + )} diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index c7adb037d8..ccc5825c38 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -33,11 +33,6 @@ const styles = composes( } `, css` - CommonDialogWrapper { - min-height: 400px; - min-width: 500px; - max-width: min-content; - } SubmittingForm { overflow: auto; &[|form] { @@ -119,6 +114,7 @@ export const AuthDialog: DialogComponent = observer(function return styled(useStyles(styles, UNDERLINE_TAB_STYLES))( = observer(function )} )} + noBodyPadding onReject={options?.persistent ? undefined : rejectDialog} > {showTabs && ( diff --git a/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx b/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx index 0a67f499f3..44eedc90de 100644 --- a/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx +++ b/webapp/packages/plugin-authentication/src/UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog.tsx @@ -29,9 +29,6 @@ const styles = composes( } `, css` - CommonDialogWrapper { - min-width: 500px; - } ErrorMessage { flex: 1; } @@ -93,6 +90,7 @@ export const ChangeUserPasswordDialog: DialogComponent = observer( return styled(style)( (function DriverSelectorDial }) { return styled(styles)( = observer(function C return styled(useStyles(styles))( = observer(function C onConnect={controller.onConnect} /> )} - noBodyPadding + noBodyPadding={controller.step === ConnectionStep.ConnectionTemplateSelect} + fixedSize noOverflow onReject={rejectDialog} > diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx index 6229425d8a..d311f9a8c9 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorConfigureDialog.tsx @@ -27,10 +27,6 @@ const styles = composes( } `, css` - CommonDialogWrapper { - max-height: 500px; - min-height: 500px; - } PropertiesTable { flex: 1; overflow: hidden; @@ -74,6 +70,7 @@ export const ProcessorConfigureDialog = observer(function ProcessorConfig return styled(useStyles(styles))( (function ProcessorConfig onCancel={onClose} /> )} + fixedSize noOverflow noBodyPadding onReject={onClose} diff --git a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx index f2a220cba4..075e2d7a3e 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/ProcessorSelectDialog.tsx @@ -19,10 +19,6 @@ import type { IExportContext } from '../IExportContext'; import { ExportProcessorList } from './ExportProcessorList/ExportProcessorList'; const styles = css` - CommonDialogWrapper { - max-height: 500px; - min-height: 500px; - } ExportProcessorList { flex: 1; } @@ -61,7 +57,9 @@ export const ProcessorSelectDialog = observer(function ProcessorSelectDia return styled(styles)( (null); +export const CellContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts index 73e81c7f55..fb9de9b3dd 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -27,4 +27,4 @@ export interface IDataGridContext { getDataGridApi: () => DataGridHandle | null; } -export const DataGridContext = createContext(null); +export const DataGridContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts index 74cb63de38..14f994b54c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts @@ -141,7 +141,7 @@ export class DataGridContextMenuCellEditingService { { id: 'row_revert', order: 7, - icon: '/icons/data_revert.svg', + icon: '/icons/data_revert_sm.svg', title: 'data_grid_table_editing_row_revert', isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx index b13be827aa..b223bb5d96 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -17,9 +17,6 @@ import { useTranslate } from '@cloudbeaver/core-localization'; import { ClipboardService } from '@cloudbeaver/core-ui'; const styles = css` - CommonDialogWrapper { - min-width: 400px; - } controls { display: flex; flex: 1; @@ -67,6 +64,7 @@ export const FilterCustomValueDialog: DialogComponent return styled(styles)( diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts index d05d6573de..a33f6ead1b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts @@ -26,4 +26,4 @@ export interface IDataGridSelectionContext { ) => void; } -export const DataGridSelectionContext = createContext(null); +export const DataGridSelectionContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx index 0a78c74c11..4d0ddba972 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx @@ -11,7 +11,7 @@ import { useContext, useState } from 'react'; import type { FormatterProps } from 'react-data-grid'; import styled, { css } from 'reshadow'; -import { useObjectRef } from '@cloudbeaver/core-blocks'; +import { getComputed, useObjectRef } from '@cloudbeaver/core-blocks'; import type { IDataPresentationActions, IResultSetElementKey, IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; import { EditingContext } from '../../Editing/EditingContext'; @@ -44,19 +44,15 @@ export const CellFormatter = observer(function CellFormatter({ className, const cellContext = useContext(CellContext); const editingContext = useContext(EditingContext); const [menuVisible, setMenuVisible] = useState(false); - const isEditing = cellContext?.isEditing ?? false; - const showCellMenu = !isEditing && ( + const isEditing = cellContext.isEditing; + const showCellMenu = getComputed(() => !isEditing && ( rest.isCellSelected || cellContext?.mouse.state.mouseEnter || menuVisible - ); + )); const spreadsheetActions = useObjectRef>({ edit(position) { - if (!tableDataContext || !editingContext) { - return; - } - const idx = tableDataContext.getColumnIndexFromColumnKey(position.column); const rowIdx = tableDataContext.getRowIndexFromKey(position.row); @@ -71,7 +67,7 @@ export const CellFormatter = observer(function CellFormatter({ className, - {showCellMenu && context && cellContext?.cell && ( + {showCellMenu && cellContext.cell && ( { export const CellFormatterFactory = observer(function CellFormatterFactory(props) { const formatterRef = useRef> | null>(null); - const context = useContext(DataGridContext); const tableDataContext = useContext(TableDataContext); const cellContext = useContext(CellContext); if (!props.isEditing || formatterRef.current === null) { formatterRef.current = TextFormatter; - if (tableDataContext && context && cellContext?.cell) { + if (cellContext.cell) { const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column); const value = tableDataContext.getCellValue(cellContext.cell); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx index 0999742dde..4fb6f8e837 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx @@ -6,36 +6,30 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useContext, useEffect, useRef } from 'react'; import type { FormatterProps } from 'react-data-grid'; -import { IconOrImage } from '@cloudbeaver/core-blocks'; +import { getComputed, IconOrImage } from '@cloudbeaver/core-blocks'; import { isValidUrl } from '@cloudbeaver/core-utils'; import { EditingContext } from '../../../Editing/EditingContext'; import { CellEditor, IEditorRef } from '../../CellEditor/CellEditor'; import { CellContext } from '../../CellRenderer/CellContext'; -import { DataGridContext } from '../../DataGridContext'; import { TableDataContext } from '../../TableDataContext'; export const TextFormatter = observer(function TextFormatter({ row, column, isCellSelected }) { const editorRef = useRef(null); - const context = useContext(DataGridContext); const editingContext = useContext(EditingContext); const tableDataContext = useContext(TableDataContext); const cellContext = useContext(CellContext); - if (!context || !tableDataContext || !editingContext || !cellContext?.cell) { + if (!cellContext.cell) { throw new Error('Contexts required'); } const formatter = tableDataContext.format; - const rawValue = useMemo( - () => computed(() => formatter.get(tableDataContext.getCellValue(cellContext!.cell!)!)), - [tableDataContext, cellContext.cell, formatter] - ).get(); + const rawValue = getComputed(() => formatter.get(tableDataContext.getCellValue(cellContext.cell!)!)); let classes = 'text-formatter'; if (rawValue === null) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index 6af69fe437..131e939ede 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -11,7 +11,7 @@ import { useContext } from 'react'; import type { HeaderRendererProps } from 'react-data-grid'; import styled, { css } from 'reshadow'; -import { StaticImage } from '@cloudbeaver/core-blocks'; +import { getComputed, StaticImage } from '@cloudbeaver/core-blocks'; import { useStyles } from '@cloudbeaver/core-theming'; import { DataGridContext } from '../DataGridContext'; @@ -66,15 +66,11 @@ export const TableColumnHeader = observer>(function Tab const tableDataContext = useContext(TableDataContext); const gridSelectionContext = useContext(DataGridSelectionContext); - if (!tableDataContext || !dataGridContext || !gridSelectionContext) { - throw new Error('One of the following contexts are missed(data grid context, grid selection context, table data context)'); - } - const resultIndex = dataGridContext.resultIndex; const model = dataGridContext.model; let icon = calculatedColumn.icon; let columnName = calculatedColumn.name as string; - const dataReadonly = tableDataContext.isReadOnly() || model.isReadonly(); + const dataReadonly = getComputed(() => tableDataContext.isReadOnly() || model.isReadonly()); let columnReadOnly = !calculatedColumn.editable; let columnTooltip: string = columnName; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts index 349717081a..659df20b5c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts @@ -51,4 +51,4 @@ export interface ITableData { isReadOnly: () => boolean; } -export const TableDataContext = createContext(null); +export const TableDataContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx index e69818e802..1126fd493e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx @@ -22,4 +22,4 @@ export interface CellPosition { rowIdx: number; } -export const EditingContext = createContext(null); +export const EditingContext = createContext(undefined as any); diff --git a/webapp/packages/plugin-data-viewer/public/icons/data_add_sm.svg b/webapp/packages/plugin-data-viewer/public/icons/data_add_sm.svg index 5161824273..1ffe959f08 100644 --- a/webapp/packages/plugin-data-viewer/public/icons/data_add_sm.svg +++ b/webapp/packages/plugin-data-viewer/public/icons/data_add_sm.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/webapp/packages/plugin-data-viewer/public/icons/data_delete_sm.svg b/webapp/packages/plugin-data-viewer/public/icons/data_delete_sm.svg index 1778f0a5ae..bc41b4f7df 100644 --- a/webapp/packages/plugin-data-viewer/public/icons/data_delete_sm.svg +++ b/webapp/packages/plugin-data-viewer/public/icons/data_delete_sm.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/webapp/packages/plugin-data-viewer/public/icons/data_revert_sm.svg b/webapp/packages/plugin-data-viewer/public/icons/data_revert_sm.svg new file mode 100644 index 0000000000..c0837739d7 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/public/icons/data_revert_sm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts index 8e19b00e7b..a915a9cd78 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction'; import type { IDatabaseDataActions } from './IDatabaseDataActions'; @@ -19,13 +19,16 @@ export class DatabaseDataActions implements IDatabaseDataActions { private actions: Map>; private source: IDatabaseDataSource; + private unCommittedActions: Map>; constructor(source: IDatabaseDataSource) { this.actions = new Map(); + this.unCommittedActions = new Map(); this.source = source; makeObservable, 'actions'>(this, { actions: observable.shallow, + updateResults: action, }); } @@ -48,17 +51,12 @@ implements IDatabaseDataActions { throw new Error('DataFormat unsupported'); } - if (!this.actions.has(result.id)) { - this.actions.set(result.id, observable.array(undefined, { deep: false })); - } - - const actionsMap = this.actions.get(result.id)!; - - let action = actionsMap.find(action => action instanceof Action); + const actions = this.getOrCreateActionsList(result.id); + let action = actions.find(action => action instanceof Action); if (!action) { action = new Action(this.source, result); - actionsMap.push(action); + this.addActionToList(result.id, actions, action); } return action as T; @@ -68,13 +66,8 @@ implements IDatabaseDataActions { result: TResult, Action: IDatabaseDataActionInterface ): T | undefined { - if (!this.actions.has(result.id)) { - this.actions.set(result.id, observable.array(undefined, { deep: false })); - } - - const actionsMap = this.actions.get(result.id)!; - - const action = actionsMap.find(action => action instanceof Action); + const actions = this.getOrCreateActionsList(result.id); + const action = actions.find(action => action instanceof Action); return action as T | undefined; } @@ -97,4 +90,51 @@ implements IDatabaseDataActions { } } } + + private addActionToList( + resultId: string, + actions: ActionsList, + action: IDatabaseDataAction + ) { + actions = observable.array([...actions, action]); + + this.unCommittedActions.set(resultId, actions); + this.scheduleCommit(resultId); + } + + private getOrCreateActionsList(resultId: string): ActionsList { + let actions = this.getActionsList(resultId); + + if (!actions) { + actions = this.createActionsList(resultId); + } + + return actions; + } + + private createActionsList(resultId: string): ActionsList { + const actions: ActionsList = observable.array(undefined, { deep: false }); + + this.unCommittedActions.set(resultId, actions); + this.scheduleCommit(resultId); + + return actions; + } + + private getActionsList(resultId: string): ActionsList | undefined { + const committed = this.actions.get(resultId); + const unCommitted = this.unCommittedActions.get(resultId); + return unCommitted ?? committed; + } + + private scheduleCommit(resultId: string) { + setTimeout(() => { + const actions = this.unCommittedActions.get(resultId); + + if (actions) { + this.actions.set(resultId, actions); + this.unCommittedActions.delete(resultId); + } + }, 1); + } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 848f6e1e3c..ad35412c59 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { observable, makeObservable } from 'mobx'; +import { observable, makeObservable, action } from 'mobx'; import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; @@ -76,6 +76,7 @@ implements IDatabaseDataSource { activeRequest: observable.ref, activeSave: observable.ref, activeTask: observable.ref, + setSupportedDataFormats: action, }); } diff --git a/webapp/packages/plugin-data-viewer/src/ScriptPreview/ScriptPreviewDialog.tsx b/webapp/packages/plugin-data-viewer/src/ScriptPreview/ScriptPreviewDialog.tsx index f5bdf2dbe0..9d4eccdd2b 100644 --- a/webapp/packages/plugin-data-viewer/src/ScriptPreview/ScriptPreviewDialog.tsx +++ b/webapp/packages/plugin-data-viewer/src/ScriptPreview/ScriptPreviewDialog.tsx @@ -22,10 +22,6 @@ import type { IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel import type { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult'; const styles = css` - CommonDialogWrapper { - min-height: 400px; - min-width: 650px; - } wrapper { display: flex; align-items: center; @@ -94,6 +90,7 @@ export const ScriptPreviewDialog = observer>(funct return styled(styles)( >( return styled(styles)(