From 49f321462e4ff29e9c839d0fd97fb99da969fa48 Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:03:33 +0200 Subject: [PATCH 01/18] CB-3454 ai (#2033) * CB-3999 add ai plugin * CB-3454 review fixes * CB-3454 gql since directive fix * CB-3454 update tsconfig * fix: npm dependencies * CB-4045 add hint property * CB-3454 add hints --------- Co-authored-by: Ainur Co-authored-by: Alexey Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../src/io/cloudbeaver/model/WebPropertyInfo.java | 5 +++++ .../io.cloudbeaver.server/schema/schema.graphqls | 2 +- .../io.cloudbeaver.server/schema/service.core.graphqls | 2 ++ webapp/packages/core-administration/package.json | 2 +- webapp/packages/core-blocks/package.json | 2 +- .../ObjectPropertyInfoForm/RenderField.tsx | 6 ++++-- webapp/packages/core-bootstrap/package.json | 2 +- webapp/packages/core-notifications/package.json | 2 +- .../src/queries/fragments/ObjectPropertyInfo.gql | 5 +++-- webapp/packages/core-ui/package.json | 2 +- webapp/packages/core-ui/src/Form/FormPart.ts | 10 +++++++++- webapp/packages/core-ui/src/Form/IFormPart.ts | 1 + webapp/packages/core-view/package.json | 2 +- webapp/packages/plugin-administration/package.json | 2 +- .../plugin-authentication-administration/package.json | 2 +- webapp/packages/plugin-authentication/package.json | 2 +- webapp/packages/plugin-codemirror6/package.json | 2 +- webapp/packages/plugin-connection-search/package.json | 2 +- .../packages/plugin-connection-template/package.json | 2 +- .../plugin-connections-administration/package.json | 2 +- webapp/packages/plugin-connections/package.json | 2 +- webapp/packages/plugin-data-export/package.json | 2 +- .../packages/plugin-data-spreadsheet-new/package.json | 2 +- .../package.json | 2 +- webapp/packages/plugin-data-viewer/package.json | 2 +- .../plugin-datasource-context-switch/package.json | 2 +- webapp/packages/plugin-ddl-viewer/package.json | 2 +- webapp/packages/plugin-devtools/package.json | 2 +- webapp/packages/plugin-gis-viewer/package.json | 2 +- webapp/packages/plugin-help/package.json | 2 +- webapp/packages/plugin-log-viewer/package.json | 2 +- webapp/packages/plugin-navigation-tabs/package.json | 2 +- webapp/packages/plugin-navigation-tree-rm/package.json | 2 +- webapp/packages/plugin-navigation-tree/package.json | 2 +- webapp/packages/plugin-object-viewer/package.json | 2 +- .../plugin-resource-manager-scripts/package.json | 2 +- webapp/packages/plugin-root/package.json | 2 +- webapp/packages/plugin-settings-menu/package.json | 2 +- webapp/packages/plugin-settings-panel/package.json | 2 +- .../plugin-sql-editor-navigation-tab/package.json | 2 +- webapp/packages/plugin-sql-editor/package.json | 2 +- webapp/packages/plugin-tools-panel/package.json | 2 +- webapp/packages/plugin-top-app-bar/package.json | 2 +- webapp/packages/plugin-user-profile/package.json | 2 +- .../plugin-version-update-administration/package.json | 2 +- .../src/PluginBootstrap.ts | 1 - 46 files changed, 63 insertions(+), 45 deletions(-) diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java index 631d002767..1d48cd397f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java @@ -88,6 +88,11 @@ public String getDescription() { } } + @Property + public String getHint() { + return property.getHint(); + } + @Property public int getOrder() { return property instanceof ObjectPropertyDescriptor ? ((ObjectPropertyDescriptor) property).getOrderNumber() : -1; diff --git a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls index afffe35fef..6f8f155a82 100644 --- a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls @@ -8,7 +8,7 @@ input PageInput { offset: Int } -directive @since(version: String!) on OBJECT|SCALAR|QUERY|MUTATION|FIELD|VARIABLE_DEFINITION|OBJECT|FIELD_DEFINITION|ARGUMENT_DEFINITION|INTERFACE|ENUM|ENUM_VALUE|INPUT_OBJECT|INPUT_FIELD_DEFINITION +directive @since(version: String!) repeatable on OBJECT|SCALAR|QUERY|MUTATION|FIELD|VARIABLE_DEFINITION|OBJECT|FIELD_DEFINITION|ARGUMENT_DEFINITION|INTERFACE|ENUM|ENUM_VALUE|INPUT_OBJECT|INPUT_FIELD_DEFINITION type Query diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 04fe3be838..59a0094df7 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -12,6 +12,8 @@ type ObjectPropertyInfo { displayName: String # Property description description: String + # Property hint + hint: String @since(version: "23.2.3") # Property category (may be used if object has a lot of properties) category: String # Property data type (int, String, etc) diff --git a/webapp/packages/core-administration/package.json b/webapp/packages/core-administration/package.json index 32f87193d2..1594db5437 100644 --- a/webapp/packages/core-administration/package.json +++ b/webapp/packages/core-administration/package.json @@ -33,7 +33,7 @@ "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.10.2", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-blocks/package.json b/webapp/packages/core-blocks/package.json index 335b8202f2..d02e576998 100644 --- a/webapp/packages/core-blocks/package.json +++ b/webapp/packages/core-blocks/package.json @@ -35,7 +35,7 @@ "react": "^18.2.0", "reakit": "~1.x.x", "reakit-utils": "^0.15.2", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index e98d588043..0acc81df68 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -158,6 +158,7 @@ export const RenderField = observer(function RenderField({ title={property.description} disabled={disabled} readOnly={readOnly} + description={property.hint} className={className} > {property.displayName ?? ''} @@ -175,6 +176,7 @@ export const RenderField = observer(function RenderField({ title={property.description} disabled={disabled} readOnly={readOnly} + description={property.hint} className={className} > {property.displayName ?? ''} @@ -225,7 +227,7 @@ export const RenderField = observer(function RenderField({ name={property.id!} state={state} defaultValue={defaultValue} - description={description} + description={description ?? property.hint} disabled={disabled} readOnly={readOnly} autoHide={autoHide} @@ -247,7 +249,7 @@ export const RenderField = observer(function RenderField({ name={property.id!} value={value} defaultValue={defaultValue} - description={description} + description={description ?? property.hint} disabled={disabled} readOnly={readOnly} autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`} diff --git a/webapp/packages/core-bootstrap/package.json b/webapp/packages/core-bootstrap/package.json index 52920e895a..94abb425de 100644 --- a/webapp/packages/core-bootstrap/package.json +++ b/webapp/packages/core-bootstrap/package.json @@ -48,7 +48,7 @@ "mobx": "^6.10.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-notifications/package.json b/webapp/packages/core-notifications/package.json index ebbaf32c30..7e20a3acf3 100644 --- a/webapp/packages/core-notifications/package.json +++ b/webapp/packages/core-notifications/package.json @@ -25,7 +25,7 @@ "mobx-react-lite": "^4.0.5", "react": "^18.2.0", "reakit": "~1.x.x", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-sdk/src/queries/fragments/ObjectPropertyInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/ObjectPropertyInfo.gql index e097d362b5..f91606e91e 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/ObjectPropertyInfo.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/ObjectPropertyInfo.gql @@ -2,6 +2,7 @@ fragment ObjectPropertyInfo on ObjectPropertyInfo { id displayName description + hint category dataType value @@ -9,5 +10,5 @@ fragment ObjectPropertyInfo on ObjectPropertyInfo { defaultValue length features - order -} \ No newline at end of file + order +} diff --git a/webapp/packages/core-ui/package.json b/webapp/packages/core-ui/package.json index f6c87c38c8..c6c290123c 100644 --- a/webapp/packages/core-ui/package.json +++ b/webapp/packages/core-ui/package.json @@ -35,7 +35,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "reakit": "~1.x.x", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-ui/src/Form/FormPart.ts b/webapp/packages/core-ui/src/Form/FormPart.ts index 78d0418a6e..b57ccee1a1 100644 --- a/webapp/packages/core-ui/src/Form/FormPart.ts +++ b/webapp/packages/core-ui/src/Form/FormPart.ts @@ -122,6 +122,10 @@ export abstract class FormPart implements IFormPar await this.load(); } + reset() { + this.setState(toJS(this.initialState)); + } + protected setInitialState(initialState: TPartState) { this.initialState = initialState; @@ -129,7 +133,11 @@ export abstract class FormPart implements IFormPar return; } - this.state = toJS(this.initialState); + this.setState(toJS(this.initialState)); + } + + protected setState(state: TPartState) { + this.state = state; } protected configure(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} diff --git a/webapp/packages/core-ui/src/Form/IFormPart.ts b/webapp/packages/core-ui/src/Form/IFormPart.ts index 76abbb8c92..98e90d00f9 100644 --- a/webapp/packages/core-ui/src/Form/IFormPart.ts +++ b/webapp/packages/core-ui/src/Form/IFormPart.ts @@ -15,4 +15,5 @@ export interface IFormPart extends ILoadableState { load(): Promise; save(): Promise; + reset(): void; } diff --git a/webapp/packages/core-view/package.json b/webapp/packages/core-view/package.json index 63dc85a7f6..d1060bbdbe 100644 --- a/webapp/packages/core-view/package.json +++ b/webapp/packages/core-view/package.json @@ -28,7 +28,7 @@ "mobx-react-lite": "^4.0.5", "react": "^18.2.0", "react-hotkeys-hook": "^4.4.1", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-administration/package.json b/webapp/packages/plugin-administration/package.json index 7b157114b1..b51c5f5d68 100644 --- a/webapp/packages/plugin-administration/package.json +++ b/webapp/packages/plugin-administration/package.json @@ -37,7 +37,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-authentication-administration/package.json b/webapp/packages/plugin-authentication-administration/package.json index f306ecbc54..285caaa0fa 100644 --- a/webapp/packages/plugin-authentication-administration/package.json +++ b/webapp/packages/plugin-authentication-administration/package.json @@ -39,7 +39,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-authentication/package.json b/webapp/packages/plugin-authentication/package.json index 49ba773ac3..8dcc484e6e 100644 --- a/webapp/packages/plugin-authentication/package.json +++ b/webapp/packages/plugin-authentication/package.json @@ -37,7 +37,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-codemirror6/package.json b/webapp/packages/plugin-codemirror6/package.json index 119c4edb69..82df9b2dad 100644 --- a/webapp/packages/plugin-codemirror6/package.json +++ b/webapp/packages/plugin-codemirror6/package.json @@ -36,7 +36,7 @@ "mobx-react-lite": "^4.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-connection-search/package.json b/webapp/packages/plugin-connection-search/package.json index 64d6c58a43..4156fb191b 100644 --- a/webapp/packages/plugin-connection-search/package.json +++ b/webapp/packages/plugin-connection-search/package.json @@ -36,7 +36,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-connection-template/package.json b/webapp/packages/plugin-connection-template/package.json index d7619661fc..ca62e25b00 100644 --- a/webapp/packages/plugin-connection-template/package.json +++ b/webapp/packages/plugin-connection-template/package.json @@ -34,7 +34,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-connections-administration/package.json b/webapp/packages/plugin-connections-administration/package.json index 3102a9eddc..d3798598b0 100644 --- a/webapp/packages/plugin-connections-administration/package.json +++ b/webapp/packages/plugin-connections-administration/package.json @@ -36,7 +36,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-connections/package.json b/webapp/packages/plugin-connections/package.json index f02ffc62cf..3abb70cbff 100644 --- a/webapp/packages/plugin-connections/package.json +++ b/webapp/packages/plugin-connections/package.json @@ -43,7 +43,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-data-export/package.json b/webapp/packages/plugin-data-export/package.json index 6548bcc967..4878c464ee 100644 --- a/webapp/packages/plugin-data-export/package.json +++ b/webapp/packages/plugin-data-export/package.json @@ -37,7 +37,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index a7e1519919..056373066d 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -38,7 +38,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-popper": "^2.3.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/package.json b/webapp/packages/plugin-data-viewer-result-set-grouping/package.json index bfcde9becf..c5ff4fc452 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/package.json +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/package.json @@ -32,7 +32,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-data-viewer/package.json b/webapp/packages/plugin-data-viewer/package.json index f9c3ed00d3..a1eda80d1d 100644 --- a/webapp/packages/plugin-data-viewer/package.json +++ b/webapp/packages/plugin-data-viewer/package.json @@ -43,7 +43,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-datasource-context-switch/package.json b/webapp/packages/plugin-datasource-context-switch/package.json index 2cfb55dd89..e46050da8d 100644 --- a/webapp/packages/plugin-datasource-context-switch/package.json +++ b/webapp/packages/plugin-datasource-context-switch/package.json @@ -35,7 +35,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-ddl-viewer/package.json b/webapp/packages/plugin-ddl-viewer/package.json index 14858c5fa6..332a193455 100644 --- a/webapp/packages/plugin-ddl-viewer/package.json +++ b/webapp/packages/plugin-ddl-viewer/package.json @@ -34,7 +34,7 @@ "@cloudbeaver/plugin-sql-editor-new": "~0.1.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-devtools/package.json b/webapp/packages/plugin-devtools/package.json index 08ccc75c2f..8796d24bcb 100644 --- a/webapp/packages/plugin-devtools/package.json +++ b/webapp/packages/plugin-devtools/package.json @@ -31,7 +31,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-gis-viewer/package.json b/webapp/packages/plugin-gis-viewer/package.json index 4242b2e889..3099385f3a 100644 --- a/webapp/packages/plugin-gis-viewer/package.json +++ b/webapp/packages/plugin-gis-viewer/package.json @@ -28,7 +28,7 @@ "mobx-react-lite": "^4.0.5", "react": "^18.2.0", "react-leaflet": "^4.2.1", - "reshadow": "~0.x.x", + "reshadow": "^0.0.1", "wellknown": "^0.5.0" }, "peerDependencies": {}, diff --git a/webapp/packages/plugin-help/package.json b/webapp/packages/plugin-help/package.json index 13a98ee3c8..8f62d61a3a 100644 --- a/webapp/packages/plugin-help/package.json +++ b/webapp/packages/plugin-help/package.json @@ -31,7 +31,7 @@ "@cloudbeaver/plugin-sql-editor": "~0.1.0", "@cloudbeaver/plugin-top-app-bar": "~0.1.0", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-log-viewer/package.json b/webapp/packages/plugin-log-viewer/package.json index ce50a8e285..6ff4a23b2b 100644 --- a/webapp/packages/plugin-log-viewer/package.json +++ b/webapp/packages/plugin-log-viewer/package.json @@ -33,7 +33,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-navigation-tabs/package.json b/webapp/packages/plugin-navigation-tabs/package.json index 16cdff7c89..91268e9929 100644 --- a/webapp/packages/plugin-navigation-tabs/package.json +++ b/webapp/packages/plugin-navigation-tabs/package.json @@ -35,7 +35,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-navigation-tree-rm/package.json b/webapp/packages/plugin-navigation-tree-rm/package.json index 0299553ed3..898dbc1971 100644 --- a/webapp/packages/plugin-navigation-tree-rm/package.json +++ b/webapp/packages/plugin-navigation-tree-rm/package.json @@ -38,7 +38,7 @@ "@cloudbeaver/plugin-resource-manager": "~0.1.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-navigation-tree/package.json b/webapp/packages/plugin-navigation-tree/package.json index 37fbe0b2a1..8f8262fba4 100644 --- a/webapp/packages/plugin-navigation-tree/package.json +++ b/webapp/packages/plugin-navigation-tree/package.json @@ -40,7 +40,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-object-viewer/package.json b/webapp/packages/plugin-object-viewer/package.json index fb7231c3b8..31d46bf291 100644 --- a/webapp/packages/plugin-object-viewer/package.json +++ b/webapp/packages/plugin-object-viewer/package.json @@ -41,7 +41,7 @@ "mobx-react-lite": "^4.0.5", "react": "^18.2.0", "reakit": "~1.x.x", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-resource-manager-scripts/package.json b/webapp/packages/plugin-resource-manager-scripts/package.json index b7d77178eb..a6c1144af9 100644 --- a/webapp/packages/plugin-resource-manager-scripts/package.json +++ b/webapp/packages/plugin-resource-manager-scripts/package.json @@ -37,7 +37,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-root/package.json b/webapp/packages/plugin-root/package.json index 83c72ec5ba..f610f5d7f7 100644 --- a/webapp/packages/plugin-root/package.json +++ b/webapp/packages/plugin-root/package.json @@ -27,7 +27,7 @@ "@cloudbeaver/core-utils": "~0.1.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-settings-menu/package.json b/webapp/packages/plugin-settings-menu/package.json index 9f32f5c42e..6a674897e8 100644 --- a/webapp/packages/plugin-settings-menu/package.json +++ b/webapp/packages/plugin-settings-menu/package.json @@ -24,7 +24,7 @@ "@cloudbeaver/plugin-top-app-bar": "~0.1.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-settings-panel/package.json b/webapp/packages/plugin-settings-panel/package.json index 2b53eb14c5..61f24e73d8 100644 --- a/webapp/packages/plugin-settings-panel/package.json +++ b/webapp/packages/plugin-settings-panel/package.json @@ -29,7 +29,7 @@ "@cloudbeaver/plugin-settings-menu": "~0.1.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/package.json b/webapp/packages/plugin-sql-editor-navigation-tab/package.json index 1b2e2d778e..3a31ed5f92 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/package.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab/package.json @@ -41,7 +41,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-sql-editor/package.json b/webapp/packages/plugin-sql-editor/package.json index d9522d9f62..b402515d72 100644 --- a/webapp/packages/plugin-sql-editor/package.json +++ b/webapp/packages/plugin-sql-editor/package.json @@ -42,7 +42,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-tools-panel/package.json b/webapp/packages/plugin-tools-panel/package.json index 4461d72a5e..eb3b07f5fe 100644 --- a/webapp/packages/plugin-tools-panel/package.json +++ b/webapp/packages/plugin-tools-panel/package.json @@ -29,7 +29,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-top-app-bar/package.json b/webapp/packages/plugin-top-app-bar/package.json index 40535958e4..0acad95a46 100644 --- a/webapp/packages/plugin-top-app-bar/package.json +++ b/webapp/packages/plugin-top-app-bar/package.json @@ -28,7 +28,7 @@ "@cloudbeaver/core-view": "~0.1.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-user-profile/package.json b/webapp/packages/plugin-user-profile/package.json index 6f712a85eb..a3f5715ec3 100644 --- a/webapp/packages/plugin-user-profile/package.json +++ b/webapp/packages/plugin-user-profile/package.json @@ -34,7 +34,7 @@ "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "reshadow": "~0.x.x" + "reshadow": "^0.0.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/plugin-version-update-administration/package.json b/webapp/packages/plugin-version-update-administration/package.json index 21540c8d0c..72cabf34e3 100644 --- a/webapp/packages/plugin-version-update-administration/package.json +++ b/webapp/packages/plugin-version-update-administration/package.json @@ -28,7 +28,7 @@ "mobx-react-lite": "^4.0.5", "react": "^18.2.0", "react-markdown": "^9.0.0", - "reshadow": "~0.x.x", + "reshadow": "^0.0.1", "semver": "^7.5.4" }, "peerDependencies": {}, diff --git a/webapp/packages/plugin-version-update-administration/src/PluginBootstrap.ts b/webapp/packages/plugin-version-update-administration/src/PluginBootstrap.ts index 266379c5f8..e1fa6720c1 100644 --- a/webapp/packages/plugin-version-update-administration/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-version-update-administration/src/PluginBootstrap.ts @@ -23,7 +23,6 @@ export class PluginBootstrap extends Bootstrap { this.administrationItemService.create({ name: 'version-update', type: AdministrationItemType.Administration, - order: 5, getContentComponent: () => VersionUpdate, getDrawerComponent: () => VersionUpdateDrawerItem, }); From abf3e7247c7181d85b0c4c90345ea495f6dba284 Mon Sep 17 00:00:00 2001 From: Alexey Date: Sun, 15 Oct 2023 23:52:04 +0200 Subject: [PATCH 02/18] chore: update yarn.lock (#2063) --- .vscode/launch.json | 4 ++-- webapp/yarn.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c94535bfbc..caa67e126d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,7 +24,7 @@ "request": "launch", "mainClass": "org.eclipse.equinox.launcher.Main", "classPaths": [ - "${workspaceFolder}/../eclipse/eclipse/plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar" + "${workspaceFolder}/../eclipse/eclipse/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar" ], "args": [ "-product", @@ -59,7 +59,7 @@ "request": "launch", "mainClass": "org.eclipse.equinox.launcher.Main", "classPaths": [ - "${workspaceFolder}/../eclipse/Eclipse.app/Contents/Eclipse/plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar" + "${workspaceFolder}/../eclipse/Eclipse.app/Contents/Eclipse/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar" ], "args": [ "-product", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 284312397d..6d08809d19 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -15618,7 +15618,7 @@ reserved-words@^0.1.2: resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" integrity sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw== -reshadow@^0.0.1, reshadow@~0.x.x: +reshadow@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/reshadow/-/reshadow-0.0.1.tgz#9a34bd7f88858836a904a22e2662ec5d1ec027b8" integrity sha512-/S0hIXfPV1ddD4yCsk1g2pzTMz03z13QYw7sTLhMQ00luV5vcHziuli7l2IlmkG/Fvyt3LD5cdFw6E4s476G5Q== From ab985673b0cce1ea071e2069c2f0353aff9ae489 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Oct 2023 15:47:11 +0200 Subject: [PATCH 03/18] CB-3704 feat(plugin-data-viewer): blob uploading (#2027) * CB-3704 feat(plugin-data-viewer): blob uploading * CB-3704 fix: check truncated value correctly * CB-4051. Added upload big files * CB-4051. remove unusable files * CB-3704 feat: upload blobs * CB-4051. Refactor after discussion with front-end * CB-4051. Merge develop * CB-4051. Fixed imports * CB-3704 fix: upload blobs for new rows * CB-4051. Fixed added new row with file * fix: reset editing state * CB-4051. Fixed after review * CB-4051. revert * CB-4051. Fixed doc, imports, style * CB-4051. Fixed checkstyle * revert * CB-4051. Refactor after review, added handler for delete temp folder * CB-3704 chore: update ts project references * CB-3704 fix: rename file upload name * CB-3704. delete comments * CB-3704. Refactor after review * CB-3074. Rename job * CB-3704. Fix save for json * CB-3704 fix: boolean value representation * CB-3704 fix: downloadable data detection * CB-3704 fix: downloadable data detection * CB-3704. Fixed boolean null value * CB-3704 fix: truncate long strings for display data * CB-3704 fix: transform complex values * CB-3704. Fixed json update * CB-3704 fix: display value * CB-3704 fix: data set * CB-3704 fix: keep content type when editing content values * CB-3704 feat: add download button for * CB-3704. Rename event --------- Co-authored-by: Denis Sinelnikov Co-authored-by: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../model/session/BaseWebSession.java | 3 + .../cloudbeaver/model/session/WebSession.java | 8 +- .../websocket/CBWebSessionEventHandler.java | 1 + .../bundles/io.cloudbeaver.server/plugin.xml | 3 + .../schema/service.events.graphqls | 3 +- .../src/io/cloudbeaver/server/CBPlatform.java | 16 ++ .../events/WSDeleteTempFileHandler.java | 50 ++++ .../server/websockets/CBEventsWebSocket.java | 1 - .../service/sql/DBWServiceSQL.java | 2 +- .../service/sql/WebSQLFileLoaderServlet.java | 101 ++++++++ .../service/sql/WebSQLProcessor.java | 49 +++- .../service/sql/WebServiceBindingSQL.java | 5 + .../service/sql/impl/WebServiceSQL.java | 6 +- .../core-blocks/src/ActionIconButton.m.css | 2 + .../src/FormControls/InputFileTextContent.tsx | 4 +- webapp/packages/core-browser/src/index.ts | 1 + .../packages/core-browser/src/selectFiles.ts | 33 +++ .../configs/webpack.product.dev.config.js | 2 +- .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/it.ts | 1 + .../core-localization/src/locales/ru.ts | 3 +- .../core-localization/src/locales/zh.ts | 1 + .../core-sdk/src/CustomGraphQLClient.ts | 50 ++-- .../uploadBlobResultSetExtension.ts | 23 ++ .../uploadDriverLibraryExtension.ts | 2 +- .../packages/core-sdk/src/GraphQLService.ts | 2 + webapp/packages/core-sdk/src/index.ts | 1 + .../packages/core-utils/src/base64ToBlob.ts | 27 ++ .../src/{blobToData.ts => blobToBase64.ts} | 6 +- webapp/packages/core-utils/src/getMIME.ts | 55 ++++ webapp/packages/core-utils/src/index.ts | 3 +- .../plugin-data-spreadsheet-new/package.json | 1 + .../src/DataGrid/CellEditor.tsx | 2 +- .../DataGrid/CellRenderer/CellRenderer.tsx | 4 + .../DataGridContextMenuCellEditingService.ts | 4 +- .../DataGridContextMenuFilterService.ts | 17 +- .../DataGridContextMenuSaveContentService.ts | 47 +++- .../src/DataGrid/DataGridTable.tsx | 3 + .../Formatters/CellFormatterFactory.tsx | 22 +- .../CellFormatters/BlobFormatter.m.css | 26 ++ .../CellFormatters/BlobFormatter.tsx | 47 ++++ .../CellFormatters/BooleanFormatter.tsx | 31 ++- .../CellFormatters/TextFormatter.m.css | 1 - .../CellFormatters/TextFormatter.tsx | 16 +- .../DataGrid/Formatters/IndexFormatter.tsx | 5 +- .../src/DataGrid/TableDataContext.ts | 2 + .../src/DataGrid/useGridSelectedCellsCopy.ts | 4 +- .../src/DataGrid/useTableData.tsx | 4 + .../plugin-data-spreadsheet-new/tsconfig.json | 3 + .../src/ContainerDataSource.ts | 45 +++- .../Actions/DatabaseEditAction.ts | 1 + .../Actions/Document/DocumentEditAction.ts | 13 + .../Actions/IDatabaseDataEditAction.ts | 7 +- .../Actions/IDatabaseDataFormatAction.ts | 9 +- .../Actions/ResultSet/IResultSetBlobValue.ts | 12 + .../ResultSet/IResultSetComplexValue.ts | 12 + .../ResultSet/IResultSetContentValue.ts | 3 +- .../Actions/ResultSet/IResultSetDataKey.ts | 2 +- .../Actions/ResultSet/IResultSetFileValue.ts | 15 ++ .../ResultSet/IResultSetGeometryValue.ts | 16 ++ .../Actions/ResultSet/ResultSetDataAction.ts | 5 +- .../ResultSet/ResultSetDataContentAction.ts | 26 +- .../ResultSet/ResultSetDataKeysUtils.ts | 10 +- .../Actions/ResultSet/ResultSetEditAction.ts | 242 ++++++++++++------ .../ResultSet/ResultSetFormatAction.ts | 185 +++++++++---- .../ResultSet/ResultSetSelectAction.ts | 6 +- .../Actions/ResultSet/ResultSetViewAction.ts | 17 +- .../ResultSet/compareResultSetRowKeys.ts | 13 + .../ResultSet/createResultSetBlobValue.ts | 16 ++ .../ResultSet/createResultSetContentValue.ts | 15 ++ .../ResultSet/createResultSetFileValue.ts | 17 ++ .../Actions/ResultSet/isResultSetBlobValue.ts | 13 + .../ResultSet/isResultSetComplexValue.ts | 12 + .../ResultSet/isResultSetContentValue.ts | 3 +- .../Actions/ResultSet/isResultSetFileValue.ts | 13 + .../ResultSet/isResultSetGeometryValue.ts | 13 + .../DatabaseDataModel/DatabaseDataActions.ts | 27 +- .../TableFooterMenu/TableFooterMenuService.ts | 2 +- .../ImageValue/ImageValuePresentation.m.css | 18 ++ .../ImageValue/ImageValuePresentation.tsx | 215 +++++++++------- .../ImageValuePresentationBootstrap.ts | 15 +- .../TextValue/TextValuePresentation.tsx | 8 +- .../packages/plugin-data-viewer/src/index.ts | 12 + .../src/IDatabaseDataGISAction.ts | 6 +- .../src/ResultSetGISAction.ts | 25 +- .../plugin-sql-editor/src/QueryDataSource.ts | 44 +++- .../src/ScriptPreview/ScriptPreviewDialog.tsx | 6 +- 87 files changed, 1390 insertions(+), 433 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java create mode 100644 webapp/packages/core-browser/src/selectFiles.ts create mode 100644 webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts create mode 100644 webapp/packages/core-utils/src/base64ToBlob.ts rename webapp/packages/core-utils/src/{blobToData.ts => blobToBase64.ts} (77%) create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index d96429c4e6..a049a6667b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -30,6 +30,7 @@ import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventDeleteTempFile; import org.jkiss.dbeaver.model.websocket.event.session.WSSessionExpiredEvent; import java.time.Instant; @@ -166,6 +167,8 @@ public synchronized WebUserContext getUserContext() { public void close() { super.close(); var sessionExpiredEvent = new WSSessionExpiredEvent(); + application.getEventController().addEvent(sessionExpiredEvent); + application.getEventController().addEvent(new WSEventDeleteTempFile(getSessionId())); synchronized (sessionEventHandlers) { for (CBWebSessionEventHandler sessionEventHandler : sessionEventHandlers) { try { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index 3dbe3c3b6e..ff50e74320 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -77,16 +77,15 @@ import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import java.lang.reflect.InvocationTargetException; import java.time.Instant; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - /** * Web session. * Is the main source of data in web application @@ -99,7 +98,6 @@ public class WebSession extends BaseWebSession public static final SMSessionType CB_SESSION_TYPE = new SMSessionType("CloudBeaver"); private static final String WEB_SESSION_AUTH_CONTEXT_TYPE = "web-session"; private static final String ATTR_LOCALE = "locale"; - private static final AtomicInteger TASK_ID = new AtomicInteger(); private final AtomicInteger taskCount = new AtomicInteger(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java index fb9e22f078..ba68ce02b2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java @@ -23,4 +23,5 @@ public interface CBWebSessionEventHandler { void handleWebSessionEvent(WSEvent event) throws DBException; void close(); + } diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index eb75b6ef1d..522dfe5427 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -73,6 +73,9 @@ + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 5d21aaee78..515b89b30d 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -51,7 +51,8 @@ enum CBEventTopic { cb_projects, cb_object_permissions, cb_subject_permissions, - cb_database_output_log + cb_database_output_log, + cb_delete_temp_folder } # Base server event interface diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 4c53999c7f..98cf78bfed 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -22,8 +22,10 @@ import io.cloudbeaver.server.jobs.WebSessionMonitorJob; import io.cloudbeaver.service.session.WebSessionManager; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Plugin; +import org.eclipse.core.runtime.Status; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -39,6 +41,7 @@ import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; import org.jkiss.dbeaver.model.qm.QMRegistry; import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.registry.BasePlatformImpl; @@ -49,6 +52,7 @@ import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; import org.jkiss.dbeaver.utils.ContentUtils; import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.IOUtils; import org.osgi.framework.Bundle; import java.io.IOException; @@ -67,6 +71,7 @@ public class CBPlatform extends BasePlatformImpl { public static final String PLUGIN_ID = "io.cloudbeaver.server"; //$NON-NLS-1$ private static final Log log = Log.getLog(CBPlatform.class); + private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files"; public static final String WORK_DATA_FOLDER_NAME = ".work-data"; @@ -159,6 +164,17 @@ protected void initialize() { new SessionStateJob(this) .scheduleMonitor(); + new AbstractJob("Delete temp folder") { + @Override + protected IStatus run(DBRProgressMonitor monitor) { + try { + IOUtils.deleteDirectory(getTempFolder(monitor, TEMP_FILE_FOLDER)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return Status.OK_STATUS; + } + }.schedule(); log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java new file mode 100644 index 0000000000..40e44c9e3a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.events; + +import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.WSEventDeleteTempFile; +import org.jkiss.utils.IOUtils; + +import java.io.IOException; +import java.nio.file.Path; + +public class WSDeleteTempFileHandler implements WSEventHandler { + + private static final Log log = Log.getLog(WSDeleteTempFileHandler.class); + private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files"; + + public void resetTempFolder(String sessionId) { + Path path = CBPlatform.getInstance() + .getTempFolder(new VoidProgressMonitor(), TEMP_FILE_FOLDER) + .resolve(sessionId); + try { + IOUtils.deleteDirectory(path); + } catch (IOException e) { + log.error("Error deleting temp path", e); + } + } + + @Override + public void handleEvent(@NotNull WSEventDeleteTempFile event) { + resetTempFolder(event.getSessionId()); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index c927564af0..401124d84c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -101,7 +101,6 @@ public void onWebSocketError(Throwable cause) { public void handleWebSessionEvent(WSEvent event) { super.handleEvent(event); } - @Override protected void handleEventException(Exception e) { super.handleEventException(e); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 163076b5a5..b14adc1e22 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -16,12 +16,12 @@ */ package io.cloudbeaver.service.sql; -import io.cloudbeaver.service.DBWService; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.DBWService; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java new file mode 100644 index 0000000000..f05434a483 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -0,0 +1,101 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.sql; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.WebServiceServletBase; +import org.eclipse.jetty.server.Request; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.json.JSONUtils; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletException; +import javax.servlet.annotation.MultipartConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@MultipartConfig +public class WebSQLFileLoaderServlet extends WebServiceServletBase { + + private static final Log log = Log.getLog(WebSQLFileLoaderServlet.class); + + private static final Type MAP_STRING_OBJECT_TYPE = new TypeToken>() { + }.getType(); + private static final String REQUEST_PARAM_VARIABLES = "variables"; + + private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files"; + + private static final String FILE_ID = "fileId"; + + private static final Gson gson = new GsonBuilder() + .serializeNulls() + .setPrettyPrinting() + .create(); + + public WebSQLFileLoaderServlet(CBApplication application) { + super(application); + } + + @Override + protected void processServiceRequest( + WebSession session, + HttpServletRequest request, + HttpServletResponse response + ) throws DBException, IOException { + if (!session.isAuthorizedInSecurityManager()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Update for users only"); + return; + } + + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return; + } + + Path tempFolder = CBPlatform.getInstance() + .getTempFolder(session.getProgressMonitor(), TEMP_FILE_FOLDER) + .resolve(session.getSessionId()); + + MultipartConfigElement multiPartConfig = new MultipartConfigElement(tempFolder.toString()); + request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multiPartConfig); + + Map variables = gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE); + + String fileId = JSONUtils.getString(variables, FILE_ID); + + if (fileId != null) { + Path file = tempFolder.resolve(fileId); + try { + Files.write(file, request.getPart("fileData").getInputStream().readAllBytes()); + } catch (ServletException e) { + log.error(e.getMessage()); + throw new DBWebException(e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index 28b3b20261..3ee8dffdd3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -16,10 +16,12 @@ */ package io.cloudbeaver.service.sql; +import com.google.gson.internal.LinkedTreeMap; import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionProvider; +import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.jobs.SqlOutputLogReaderJob; import org.eclipse.jface.text.Document; import org.jkiss.code.NotNull; @@ -52,10 +54,12 @@ import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; /** * Web SQL processor. @@ -66,6 +70,9 @@ public class WebSQLProcessor implements WebSessionProvider { private static final int MAX_RESULTS_COUNT = 100; + private static final String FILE_ID = "fileId"; + private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files"; + private final WebSession webSession; private final WebConnectionInfo connection; private final SQLSyntaxManager syntaxManager; @@ -475,7 +482,7 @@ private DBSDataManipulator generateUpdateResultsDataBatch( for (WebSQLResultsRow row : updatedRows) { Map updateValues = row.getUpdateValues().entrySet().stream() .filter(x -> CommonUtils.equalObjects(allAttributes[CommonUtils.toInt(x.getKey())].getRowIdentifier(), rowIdentifier)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + .collect(HashMap::new, (m,v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll); if (CommonUtils.isEmpty(row.getData()) || CommonUtils.isEmpty(updateValues)) { continue; } @@ -492,8 +499,8 @@ private DBSDataManipulator generateUpdateResultsDataBatch( Object[] rowValues = new Object[updateAttributes.length + keyAttributes.length]; for (int i = 0; i < updateAttributes.length; i++) { DBDAttributeBinding updateAttribute = updateAttributes[i]; - Object realCellValue = convertInputCellValue(session, updateAttribute, - updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition())), withoutExecution); + Object value = updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition())); + Object realCellValue = setCellRowValue(value, webSession, session, updateAttribute, withoutExecution); rowValues[i] = realCellValue; finalRow[updateAttribute.getOrdinalPosition()] = realCellValue; } @@ -539,8 +546,14 @@ private DBSDataManipulator generateUpdateResultsDataBatch( for (int i = 0; i < allAttributes.length; i++) { if (addedValues.get(i) != null) { - Object realCellValue = convertInputCellValue(session, allAttributes[i], - addedValues.get(i), withoutExecution); + Object realCellValue; + if (addedValues.get(i) instanceof LinkedTreeMap) { + LinkedTreeMap variables = (LinkedTreeMap) addedValues.get(i); + realCellValue = setCellRowValue(variables, webSession, session, allAttributes[i], withoutExecution); + } else { + realCellValue = convertInputCellValue(session, allAttributes[i], + addedValues.get(i), withoutExecution); + } insertAttributes.put(allAttributes[i], realCellValue); finalRow[i] = realCellValue; } @@ -928,4 +941,28 @@ private static int resolveMaxResultsCount(@Nullable DBPDataSource dataSource) { private static DBCExecutionPurpose resolveQueryPurpose(DBDDataFilter filter) { return filter.hasFilters() ? DBCExecutionPurpose.USER_FILTERED : DBCExecutionPurpose.USER; } + + private Object setCellRowValue(Object cellRow, WebSession webSession, DBCSession dbcSession, DBDAttributeBinding allAttributes, boolean withoutExecution) { + if (cellRow instanceof LinkedTreeMap) { + LinkedTreeMap variables = (LinkedTreeMap) cellRow; + if (variables.get(FILE_ID) != null) { + Path path = CBPlatform.getInstance() + .getTempFolder(webSession.getProgressMonitor(), TEMP_FILE_FOLDER) + .resolve(webSession.getSessionId()) + .resolve(variables.get(FILE_ID).toString()); + + try { + var file = Files.newInputStream(path); + return convertInputCellValue(dbcSession, allAttributes, file, withoutExecution); + } catch (IOException | DBCException e) { + return new DBException(e.getMessage()); + } + } + } + try { + return convertInputCellValue(dbcSession, allAttributes, cellRow, withoutExecution); + } catch (DBCException e) { + return new DBException(e.getMessage()); + } + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index fa8d1ed3cf..9b63060156 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -255,6 +255,11 @@ public void addServlets(CBApplication application, DBWServletContext servletCont new WebSQLResultServlet(application, getServiceImpl()), application.getServicesURI() + "sql-result-value/*" ); + servletContext.addServlet( + "sqlUploadFile", + new WebSQLFileLoaderServlet(application), + application.getServicesURI() + "resultset/blob/*" + ); } private static class WebSQLConfiguration { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 2e1f5f6de0..94229ea4ea 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -83,7 +83,7 @@ public WebSQLContextInfo[] listContexts( conToRead.addAll(session.getConnections()); } - List contexts = new ArrayList<>(); + List contexts = new ArrayList<>(); for (WebConnectionInfo con : conToRead) { WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(con, false); if (sqlProcessor != null) { @@ -334,7 +334,7 @@ public String readLobValue( monitor -> { try { result.append(contextInfo.getProcessor().readLobValue( - monitor, contextInfo, resultsId, lobColumnIndex, row.get(0))); + monitor, contextInfo, resultsId, lobColumnIndex, row.get(0))); } catch (Exception e) { throw new InvocationTargetException(e); } @@ -404,7 +404,7 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException, In DBSDataContainer dataContainer = contextInfo.getProcessor().getDataContainerByNodePath( monitor, nodePath, DBSDataContainer.class); - WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer( + WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer( contextInfo, monitor, dataContainer, diff --git a/webapp/packages/core-blocks/src/ActionIconButton.m.css b/webapp/packages/core-blocks/src/ActionIconButton.m.css index 62d354bcc9..5dce1ceef1 100644 --- a/webapp/packages/core-blocks/src/ActionIconButton.m.css +++ b/webapp/packages/core-blocks/src/ActionIconButton.m.css @@ -7,4 +7,6 @@ height: 24px !important; overflow: hidden; flex-shrink: 0; + flex-grow: 0; + flex-basis: auto; } diff --git a/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx b/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx index 258436c234..e734d071ad 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import { ReactNode, useContext, useState } from 'react'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; -import { blobToData, bytesToSize } from '@cloudbeaver/core-utils'; +import { blobToBase64, bytesToSize } from '@cloudbeaver/core-utils'; import { Button } from '../Button'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; @@ -121,7 +121,7 @@ export const InputFileTextContent: InputFileTextContentType = observer(function try { validateFileSize(file.size); - const value = await blobToData(file); + const value = await blobToBase64(file); if (value) { setSelected(file); diff --git a/webapp/packages/core-browser/src/index.ts b/webapp/packages/core-browser/src/index.ts index 231b868af1..4afcee8d0c 100644 --- a/webapp/packages/core-browser/src/index.ts +++ b/webapp/packages/core-browser/src/index.ts @@ -1,4 +1,5 @@ export * from './IndexedDB/IndexedDBService'; export * from './IndexedDB/IndexedDB'; export * from './manifest'; +export * from './selectFiles'; export * from './ServiceWorkerService'; diff --git a/webapp/packages/core-browser/src/selectFiles.ts b/webapp/packages/core-browser/src/selectFiles.ts new file mode 100644 index 0000000000..3c15074e7e --- /dev/null +++ b/webapp/packages/core-browser/src/selectFiles.ts @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function selectFiles(callback: (files: FileList | null) => any): void { + let removed = false; + const input = document.createElement('input'); + input.type = 'file'; + input.onchange = () => { + callback(input.files); + removed = true; + input.remove(); + }; + input.style.position = 'fixed'; + input.style.top = '-100px'; + input.style.left = '-100px'; + input.style.opacity = '0'; + input.style.pointerEvents = 'none'; + input.style.zIndex = '-1'; + document.body.append(input); + + input.click(); + + setTimeout(() => { + if (!removed) { + input.remove(); + } + }, 30 * 60 * 1000); +} diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index c6cafab724..102370cd7b 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -76,7 +76,7 @@ module.exports = (env, argv) => { const logger = devServer.compiler.getInfrastructureLogger('webpack-dev-server'); const port = devServer.server.address().port; logger.info(`Proxy from http://localhost:8080 to http://127.0.0.1:${port}`); - httpProxy.createProxyServer({ target:`http://127.0.0.1:${port}` }).listen(8080); + httpProxy.createProxyServer({ target:`http://127.0.0.1:${port}`, secure: false }).listen(8080); }, }, plugins: [ diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 763f0dc9f6..14498d138d 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -92,6 +92,7 @@ export default [ ['ui_close_all_to_the_left', 'Close all to the Left'], ['ui_or', 'Or'], ['ui_download', 'Download'], + ['ui_upload', 'Upload'], ['ui_import', 'Import'], ['ui_view', 'View'], ['ui_limit', 'Limit'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index d0763e2198..2c8a057faf 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -76,6 +76,7 @@ export default [ ['ui_close_all_to_the_left', 'Close all to the Left'], ['ui_or', 'Or'], ['ui_download', 'Download'], + ['ui_upload', 'Upload'], ['ui_import', 'Import'], ['ui_view', 'View'], ['ui_limit', 'Limit'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 73252113e5..a61d30fd4f 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -87,7 +87,8 @@ export default [ ['ui_close_all_to_the_right', 'Закрыть все справа'], ['ui_close_all_to_the_left', 'Закрыть все слева'], ['ui_or', 'Или'], - ['ui_download', 'Загрузить'], + ['ui_download', 'Cкачать'], + ['ui_upload', 'Загрузить'], ['ui_import', 'Импортировать'], ['ui_view', 'Смотреть'], ['ui_limit', 'Лимит'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index 02bc0e4c7c..dcd8b9e5a2 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -89,6 +89,7 @@ export default [ ['ui_close_all_to_the_left', 'Close all to the Left'], ['ui_or', 'Or'], ['ui_download', 'Download'], + ['ui_upload', 'Upload'], ['ui_import', 'Import'], ['ui_view', 'View'], ['ui_limit', 'Limit'], diff --git a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts index 0714a8448d..5ce68ed447 100644 --- a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts +++ b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts @@ -31,6 +31,19 @@ export class CustomGraphQLClient extends GraphQLClient { private requestsBlockedReason: Error | string | null = null; async uploadFile( + url: string, + file: Blob, + query?: string, + variables?: V, + onUploadProgress?: (event: UploadProgressEvent) => void, + ): Promise { + return this.interceptors.reduce( + (accumulator, interceptor) => interceptor(accumulator), + this.overrideFilesUpload(url, file, query, variables, onUploadProgress), + ); + } + + async uploadFiles( url: string, files: FileList, query?: string, @@ -39,7 +52,7 @@ export class CustomGraphQLClient extends GraphQLClient { ): Promise { return this.interceptors.reduce( (accumulator, interceptor) => interceptor(accumulator), - this.overrideFileUpload(url, files, query, variables, onUploadProgress), + this.overrideFilesUpload(url, files, query, variables, onUploadProgress), ); } @@ -107,9 +120,9 @@ export class CustomGraphQLClient extends GraphQLClient { } } - private async overrideFileUpload( + private async overrideFilesUpload( url: string, - files: FileList, + files: FileList | Blob, query?: string, variables?: V, onUploadProgress?: (event: UploadProgressEvent) => void, @@ -118,19 +131,24 @@ export class CustomGraphQLClient extends GraphQLClient { try { const { operationName } = resolveRequestDocument(query ?? ''); // TODO: we don't support GQL response right now - const response = await axios.postForm/**/ ( - url, - { - operationName, - query, - variables: JSON.stringify(variables), - 'files[]': files, - }, - { - onUploadProgress, - responseType: 'json', - }, - ); + const data = { + operationName, + query, + variables: JSON.stringify(variables), + 'files[]': undefined as any, + fileData: undefined as any, + }; + + if (files instanceof FileList) { + data['files[]'] = files; + } else { + data.fileData = files; + } + + const response = await axios.postForm/**/ (url, data, { + onUploadProgress, + responseType: 'json', + }); // TODO: we don't support GQL response right now // TODO: seems here can be undefined diff --git a/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts b/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts new file mode 100644 index 0000000000..c80271ed8c --- /dev/null +++ b/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { GlobalConstants } from '@cloudbeaver/core-utils'; + +import type { CustomGraphQLClient, UploadProgressEvent } from '../CustomGraphQLClient'; + +export interface IUploadDriverLibraryExtension { + uploadBlobResultSet: (fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void) => Promise; +} + +export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension { + return { + uploadBlobResultSet(fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void): Promise { + // api/resultset/blob + return client.uploadFile(GlobalConstants.absoluteServiceUrl('resultset', 'blob'), data, undefined, { fileId }, onUploadProgress); + }, + }; +} diff --git a/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts b/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts index 7a239f6bf8..3f088bdce5 100644 --- a/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts +++ b/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts @@ -16,7 +16,7 @@ export interface IUploadDriverLibraryExtension { export function uploadDriverLibraryExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension { return { uploadDriverLibrary(driverId: string, files: FileList, onUploadProgress?: (event: UploadProgressEvent) => void): Promise { - return client.uploadFile(GlobalConstants.absoluteServiceUrl('drivers', 'library'), files, undefined, { driverId }, onUploadProgress); + return client.uploadFiles(GlobalConstants.absoluteServiceUrl('drivers', 'library'), files, undefined, { driverId }, onUploadProgress); }, }; } diff --git a/webapp/packages/core-sdk/src/GraphQLService.ts b/webapp/packages/core-sdk/src/GraphQLService.ts index 07cdc795ae..b8eb8e9d7e 100644 --- a/webapp/packages/core-sdk/src/GraphQLService.ts +++ b/webapp/packages/core-sdk/src/GraphQLService.ts @@ -9,6 +9,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { CustomGraphQLClient } from './CustomGraphQLClient'; import { EnvironmentService } from './EnvironmentService'; +import { uploadBlobResultSetExtension } from './Extensions/uploadBlobResultSetExtension'; import { uploadDriverLibraryExtension } from './Extensions/uploadDriverLibraryExtension'; import type { IResponseInterceptor } from './IResponseInterceptor'; import { getSdk } from './sdk'; @@ -19,6 +20,7 @@ function extendedSDK(client: CustomGraphQLClient) { return { ...sdk, ...uploadDriverLibraryExtension(client), + ...uploadBlobResultSetExtension(client), }; } diff --git a/webapp/packages/core-sdk/src/index.ts b/webapp/packages/core-sdk/src/index.ts index 399400d300..de2ffb2c59 100644 --- a/webapp/packages/core-sdk/src/index.ts +++ b/webapp/packages/core-sdk/src/index.ts @@ -1,5 +1,6 @@ export * from './AsyncTask/AsyncTask'; export * from './AsyncTask/AsyncTaskInfoService'; +export * from './Extensions/uploadBlobResultSetExtension'; export * from './CustomGraphQLClient'; export * from './DetailsError'; export * from './EnvironmentService'; diff --git a/webapp/packages/core-utils/src/base64ToBlob.ts b/webapp/packages/core-utils/src/base64ToBlob.ts new file mode 100644 index 0000000000..54bd599203 --- /dev/null +++ b/webapp/packages/core-utils/src/base64ToBlob.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function base64ToBlob(base64: string, mime = 'application/octet-stream', partSize = 512): Blob { + const byteCharacters = atob(base64); + const byteArrays = []; + let slice: string; + + for (let offset = 0; offset < byteCharacters.length; offset += partSize) { + slice = byteCharacters.slice(offset, offset + partSize); + + const byteNumbers = new Array(slice.length); + + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + byteArrays.push(new Uint8Array(byteNumbers)); + } + + return new Blob(byteArrays, { type: mime }); +} diff --git a/webapp/packages/core-utils/src/blobToData.ts b/webapp/packages/core-utils/src/blobToBase64.ts similarity index 77% rename from webapp/packages/core-utils/src/blobToData.ts rename to webapp/packages/core-utils/src/blobToBase64.ts index 480c9bce7a..36e47575a5 100644 --- a/webapp/packages/core-utils/src/blobToData.ts +++ b/webapp/packages/core-utils/src/blobToBase64.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -export function blobToData(blob: Blob | File): Promise { +export function blobToBase64(blob: Blob | File, slice?: number): Promise { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = () => { @@ -16,6 +16,10 @@ export function blobToData(blob: Blob | File): Promise { reject(fileReader.error); }; + if (slice) { + blob = blob.slice(0, slice); + } + fileReader.readAsDataURL(blob); }); } diff --git a/webapp/packages/core-utils/src/getMIME.ts b/webapp/packages/core-utils/src/getMIME.ts index 64a3727cf5..dd8bc12444 100644 --- a/webapp/packages/core-utils/src/getMIME.ts +++ b/webapp/packages/core-utils/src/getMIME.ts @@ -24,3 +24,58 @@ export function getMIME(binary: string): string | null { return null; } } + +// function getMimeType(blob: Blob, callback) { +// const fileReader = new FileReader(); + +// fileReader.onloadend = function (event) { +// let mimeType = ''; + +// const arr = new Uint8Array(event.target.result).subarray( +// 0, +// 4, +// ); +// let header = ''; + +// for (let index = 0; index < arr.length; index++) { +// header += arr[index].toString(16); +// } + +// // View other byte signature patterns here: +// // 1) https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern +// // 2) https://en.wikipedia.org/wiki/List_of_file_signatures +// switch (header) { +// case '89504e47': { +// mimeType = 'image/png'; +// break; +// } +// case '47494638': { +// mimeType = 'image/gif'; +// break; +// } +// case '52494646': +// case '57454250': +// mimeType = 'image/webp'; +// break; +// case '49492A00': +// case '4D4D002A': +// mimeType = 'image/tiff'; +// break; +// case 'ffd8ffe0': +// case 'ffd8ffe1': +// case 'ffd8ffe2': +// case 'ffd8ffe3': +// case 'ffd8ffe8': +// mimeType = 'image/jpeg'; +// break; +// default: { +// mimeType = blob.type; +// break; +// } +// } + +// callback(mimeType); +// }; + +// fileReader.readAsArrayBuffer(blob.slice(0, 4)); +// } diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index d5646f9b6f..ca2a5a0ccc 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -8,7 +8,8 @@ export * from './Quadtree/index'; export * from './underscore'; -export * from './blobToData'; +export * from './base64ToBlob'; +export * from './blobToBase64'; export * from './bytesToSize'; export * from './cacheValue'; export * from './clsx'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index 056373066d..eeefbbc709 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -30,6 +30,7 @@ "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", + "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/plugin-data-viewer": "~0.1.0", "@cloudbeaver/plugin-react-data-grid": "~0.1.0", "@popperjs/core": "^2.11.8", diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx index ffcc6095ec..a7f0f01721 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx @@ -72,7 +72,7 @@ export const CellEditor = observer, ' const cellKey: IResultSetElementKey = { row, column: column.columnDataIndex }; - const value = tableDataContext.format.getText(tableDataContext.getCellValue(cellKey)!) ?? ''; + const value = tableDataContext.format.getText(cellKey); const handleSave = () => onClose(false); const handleReject = () => { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx index e86adf0b1b..a6ff25b0ca 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx @@ -90,6 +90,10 @@ export const CellRenderer = observer> { const { model, resultIndex, key } = context.data; const data = model.source.getAction(resultIndex, ResultSetDataAction); - const format = model.source.getAction(resultIndex, ResultSetFormatAction); const supportedOperations = data.getColumnOperations(key.column); const columnLabel = data.getColumn(key.column)?.label || ''; @@ -100,8 +99,7 @@ export class DataGridContextMenuFilterService { }, titleGetter() { const val = typeof value === 'function' ? value() : value; - const stringifyValue = format.toDisplayString(val); - const wrappedValue = wrapOperationArgument(operation.id, stringifyValue); + const wrappedValue = wrapOperationArgument(operation.id, val); const clippedValue = replaceMiddle(wrappedValue, ' ... ', 8, 30); return `${columnLabel} ${operation.expression} ${clippedValue}`; }, @@ -220,14 +218,14 @@ export class DataGridContextMenuFilterService { const supportedOperations = data.getColumnOperations(key.column); const value = data.getCellValue(key); - return value === undefined || supportedOperations.length === 0 || format.isNull(value); + return value === undefined || supportedOperations.length === 0 || format.isNull(key); }, panel: new ComputedContextMenuModel({ id: 'cellValuePanel', menuItemsGetter: context => { const { model, resultIndex, key } = context.data; - const data = model.source.getAction(resultIndex, ResultSetDataAction); - const cellValue = data.getCellValue(key); + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const cellValue = format.getText(key); const items = this.getGeneralizedMenuItems(context, cellValue, 'filter'); return items; }, @@ -253,10 +251,8 @@ export class DataGridContextMenuFilterService { id: 'customValuePanel', menuItemsGetter: context => { const { model, resultIndex, key } = context.data; - const format = model.source.getAction(resultIndex, ResultSetFormatAction); const data = model.source.getAction(resultIndex, ResultSetDataAction); const supportedOperations = data.getColumnOperations(key.column); - const cellValue = data.getCellValue(key) ?? ''; const columnLabel = data.getColumn(key.column)?.label || ''; return supportedOperations @@ -273,9 +269,10 @@ export class DataGridContextMenuFilterService { title: title + ' ..', icon: 'filter-custom', onClick: async () => { - const stringifyCellValue = format.toDisplayString(cellValue); + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const displayString = format.getText(key); const customValue = await this.commonDialogService.open(FilterCustomValueDialog, { - defaultValue: stringifyCellValue, + defaultValue: displayString, inputTitle: title + ':', }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts index 616af6741b..9766f1ae80 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts @@ -5,25 +5,26 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { selectFiles } from '@cloudbeaver/core-browser'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ResultSetDataContentAction, ResultSetDataKeysUtils } from '@cloudbeaver/plugin-data-viewer'; +import { + createResultSetBlobValue, + ResultSetDataContentAction, + ResultSetDataKeysUtils, + ResultSetEditAction, + ResultSetFormatAction, +} from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuService } from './DataGridContextMenuService'; @injectable() export class DataGridContextMenuSaveContentService { - private static readonly menuContentSaveToken = 'menuContentSave'; - constructor(private readonly dataGridContextMenuService: DataGridContextMenuService, private readonly notificationService: NotificationService) {} - getMenuContentSaveToken(): string { - return DataGridContextMenuSaveContentService.menuContentSaveToken; - } - register(): void { this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuContentSaveToken(), + id: 'menuContentDownload', order: 4, title: 'ui_download', icon: '/icons/export.svg', @@ -45,6 +46,36 @@ export class DataGridContextMenuSaveContentService { isDisabled: context => { const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); + return ( + context.data.model.isLoading() || + (!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement)) + ); + }, + }); + this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { + id: 'menuContentUpload', + order: 5, + title: 'ui_upload', + icon: '/icons/import.svg', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + onClick: async context => { + selectFiles(files => { + const edit = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction); + const file = files?.item(0) ?? undefined; + if (file) { + edit.set(context.data.key, createResultSetBlobValue(file)); + } + }); + }, + isHidden: context => { + const format = context.data.model.source.getAction(context.data.resultIndex, ResultSetFormatAction); + return !format.isBinary(context.data.key); + }, + isDisabled: context => { + const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); + return ( context.data.model.isLoading() || (!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement)) diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index e3dd643458..b4ce46fe97 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -319,6 +319,7 @@ export const DataGridTable = observer(function CellFormatterFacto formatterRef.current = TextFormatter; if (cellContext.cell) { - const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column); - const value = tableDataContext.getCellValue(cellContext.cell); - - if (value !== undefined) { - const rawValue = tableDataContext.format.get(value); - - if (resultColumn && isBooleanValuePresentationAvailable(rawValue, resultColumn)) { - formatterRef.current = BooleanFormatter; + const isBlob = tableDataContext.format.isBinary(cellContext.cell); + + if (isBlob) { + formatterRef.current = BlobFormatter; + } else { + const value = tableDataContext.getCellValue(cellContext.cell); + if (value !== undefined) { + const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column); + const rawValue = tableDataContext.format.get(cellContext.cell); + + if (resultColumn && isBooleanValuePresentationAvailable(rawValue, resultColumn)) { + formatterRef.current = BooleanFormatter; + } } } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css new file mode 100644 index 0000000000..696c9aaac1 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.blobFormatter { + display: flex; + align-items: center; + &:not(.nullValue) { + color: var(--theme-primary); + } +} + +.blobFormatterValue { + text-transform: uppercase; + overflow: hidden; + white-space: pre; + text-overflow: ellipsis; +} + +.nullValue { + composes: nullValue from './CellNullValue.m.css'; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx new file mode 100644 index 0000000000..91de76315e --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx @@ -0,0 +1,47 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { observer } from 'mobx-react-lite'; +import { useContext } from 'react'; + +import { getComputed, s, useS } from '@cloudbeaver/core-blocks'; +import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; +import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; + +import { EditingContext } from '../../../Editing/EditingContext'; +import { CellContext } from '../../CellRenderer/CellContext'; +import { DataGridContext } from '../../DataGridContext'; +import { TableDataContext } from '../../TableDataContext'; +import style from './BlobFormatter.m.css'; + +export const BlobFormatter = observer>(function BlobFormatter({ column, row }) { + const context = useContext(DataGridContext); + const tableDataContext = useContext(TableDataContext); + const editingContext = useContext(EditingContext); + const cellContext = useContext(CellContext); + const cell = cellContext.cell; + + if (!context || !tableDataContext || !editingContext || !cell) { + throw new Error('Contexts required'); + } + + const styles = useS(style); + + const formatter = tableDataContext.format; + const rawValue = getComputed(() => formatter.get(cell)); + const displayString = getComputed(() => formatter.getDisplayString(cell)); + + const nullValue = rawValue === null; + const disabled = !column.editable || editingContext.readonly || formatter.isReadOnly(cell); + const readonly = tableDataContext.isCellReadonly(cell); + + return ( + +
{displayString}
+
+ ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx index 61d805ce2a..5cb32cca7e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx @@ -5,11 +5,10 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useContext, useMemo } from 'react'; +import { useContext } from 'react'; -import { s, useS } from '@cloudbeaver/core-blocks'; +import { getComputed, s, useS } from '@cloudbeaver/core-blocks'; import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; @@ -25,35 +24,35 @@ export const BooleanFormatter = observer>(func const editingContext = useContext(EditingContext); const cellContext = useContext(CellContext); - if (!context || !tableDataContext || !editingContext || !cellContext.cell) { + const cell = cellContext.cell; + + if (!context || !tableDataContext || !editingContext || !cell) { throw new Error('Contexts required'); } const styles = useS(style); const formatter = tableDataContext.format; - const rawValue = useMemo( - () => computed(() => formatter.get(tableDataContext.getCellValue(cellContext!.cell!)!)), - [tableDataContext, cellContext.cell, formatter], - ).get(); - const value = typeof rawValue === 'string' ? rawValue.toLowerCase() === 'true' : rawValue; - const stringifiedValue = formatter.toDisplayString(value); - const valueRepresentation = value === null ? stringifiedValue : `[${value ? 'v' : ' '}]`; - const disabled = !column.editable || editingContext.readonly || formatter.isReadOnly(cellContext.cell); + const value = getComputed(() => formatter.get(cell)); + const textValue = getComputed(() => formatter.getText(cell)); + const booleanValue = getComputed(() => textValue.toLowerCase() === 'true'); + const stringifiedValue = getComputed(() => formatter.getDisplayString(cell)); + const valueRepresentation = value === null ? stringifiedValue : `[${booleanValue ? 'v' : ' '}]`; + const disabled = !column.editable || editingContext.readonly || formatter.isReadOnly(cell); function toggleValue() { - if (disabled || !tableDataContext || !cellContext.cell) { + if (disabled || !tableDataContext || !cell) { return; } - const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column); + const resultColumn = tableDataContext.getColumnInfo(cell.column); if (!resultColumn) { return; } - const nextValue = !resultColumn.required && value === false ? null : !value; + const nextValue = !resultColumn.required && value === false ? null : !booleanValue; - tableDataContext.editor.set(cellContext.cell, nextValue); + tableDataContext.editor.set(cell, nextValue); } return ( diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css index 9bc33bd293..aa2b666fe8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css @@ -27,7 +27,6 @@ .textFormatterValue { overflow: hidden; - white-space: pre; text-overflow: ellipsis; } 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 2b3ce9d003..5095124c29 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 @@ -31,12 +31,12 @@ export const TextFormatter = observer>(functio const style = useS(styles); const formatter = tableDataContext.format; - const rawValue = getComputed(() => formatter.get(tableDataContext.getCellValue(cellContext.cell!)!)); + const rawValue = getComputed(() => formatter.get(cellContext.cell!)); + const textValue = formatter.getText(cellContext.cell!); + const displayValue = formatter.getDisplayString(cellContext.cell!); const classes = s(style, { textFormatter: true, nullValue: rawValue === null }); - const value = formatter.toDisplayString(rawValue); - const handleClose = useCallback(() => { editingContext.closeEditor(cellContext.position); }, [cellContext]); @@ -58,16 +58,14 @@ export const TextFormatter = observer>(functio ); } - const isUrl = typeof rawValue === 'string' && isValidUrl(rawValue); - return ( -
- {isUrl && ( - +
+ {isValidUrl(textValue) && ( + )} -
{value}
+
{displayValue}
); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx index 70248aa345..5e539bbcbc 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx @@ -5,6 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer'; @@ -12,8 +13,8 @@ import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid'; import { CellContext } from '../CellRenderer/CellContext'; -export const IndexFormatter: React.FC> = function IndexFormatter(props) { +export const IndexFormatter: React.FC> = observer(function IndexFormatter(props) { const context = useContext(CellContext); return
{context.position.rowIdx + 1}
; -}; +}); 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 c1d6558471..f56f2def1b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts @@ -16,6 +16,7 @@ import type { IResultSetValue, ResultSetConstraintAction, ResultSetDataAction, + ResultSetDataContentAction, ResultSetEditAction, ResultSetFormatAction, ResultSetViewAction, @@ -37,6 +38,7 @@ interface IColumnMetrics { export interface ITableData { format: ResultSetFormatAction; + dataContent: ResultSetDataContentAction; data: ResultSetDataAction; editor: ResultSetEditAction; view: ResultSetViewAction; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 1dd5ad7aaa..eb1c486d01 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -20,9 +20,7 @@ const EVENT_KEY_CODE = { }; function getCellCopyValue(tableData: ITableData, key: IResultSetElementKey): string { - const cell = tableData.getCellValue(key); - const cellValue = cell !== undefined ? tableData.format.getText(cell) : undefined; - return cellValue ?? ''; + return tableData.format.getText(key); } function getSelectedCellsValue(tableData: ITableData, selectedCells: Map) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx index 3167d1e2a4..a33991a9f5 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx @@ -17,6 +17,7 @@ import { IResultSetRowKey, ResultSetConstraintAction, ResultSetDataAction, + ResultSetDataContentAction, ResultSetDataKeysUtils, ResultSetEditAction, ResultSetFormatAction, @@ -58,6 +59,7 @@ export function useTableData( const data = model.source.getAction(resultIndex, ResultSetDataAction); const editor = model.source.getAction(resultIndex, ResultSetEditAction); const view = model.source.getAction(resultIndex, ResultSetViewAction); + const dataContent = model.source.getAction(resultIndex, ResultSetDataContentAction); const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); return useObservableRef }>( @@ -188,6 +190,7 @@ export function useTableData( rows: computed, columnKeys: computed, format: observable.ref, + dataContent: observable.ref, data: observable.ref, editor: observable.ref, view: observable.ref, @@ -196,6 +199,7 @@ export function useTableData( }, { format, + dataContent, data, editor, view, diff --git a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json index 2764c2ed85..6a2934cf7e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json +++ b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json @@ -42,6 +42,9 @@ { "path": "../core-utils/tsconfig.json" }, + { + "path": "../core-browser/tsconfig.json" + }, { "path": "../plugin-data-viewer/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 04adc6951a..1c98fba0b0 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -18,8 +18,10 @@ import { SqlQueryResults, UpdateResultsDataBatchMutationVariables, } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; import { DocumentEditAction } from './DatabaseDataModel/Actions/Document/DocumentEditAction'; +import type { IResultSetBlobValue } from './DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue'; import { ResultSetEditAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource'; import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions'; @@ -154,31 +156,42 @@ export class ContainerDataSource extends DatabaseDataSource newResult.id === result.id, @@ -188,7 +201,15 @@ export class ContainerDataSource extends DatabaseDataSource { @@ -54,6 +54,7 @@ export interface IDatabaseDataEditAction void; duplicate: (...key: TKey[]) => void; delete: (key: TKey) => void; + applyPartialUpdate(result: TResult): void; applyUpdate: (result: TResult) => void; revert: (key: TKey) => void; clear: () => void; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts index 997cd7eebc..790f553ca7 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts @@ -10,8 +10,9 @@ import type { IDatabaseDataResult } from '../IDatabaseDataResult'; export interface IDatabaseDataFormatAction extends IDatabaseDataAction { isReadOnly: (key: TKey) => boolean; - get: (value: any) => any; - getText: (value: any) => string | null; - isNull: (value: any) => boolean; - toDisplayString: (value: any) => string; + isNull: (key: TKey) => boolean; + isBinary: (key: TKey) => boolean; + get: (key: TKey) => any; + getText: (key: TKey) => string; + getDisplayString: (key: TKey) => string; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts new file mode 100644 index 0000000000..a99369c92d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetFileValue } from './IResultSetFileValue'; + +export interface IResultSetBlobValue extends IResultSetFileValue { + blob: Blob; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts new file mode 100644 index 0000000000..04695b7496 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface IResultSetComplexValue { + $type: string; + value?: any; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts index 8f32f4d009..31fe64404b 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts @@ -5,8 +5,9 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import type { IResultSetComplexValue } from './IResultSetComplexValue'; -export interface IResultSetContentValue { +export interface IResultSetContentValue extends IResultSetComplexValue { $type: 'content'; binary?: string; text?: string; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts index 2a06c2ed4c..645863f396 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts @@ -12,7 +12,7 @@ export interface IResultSetColumnKey { export interface IResultSetRowKey { index: number; - key?: string; + subIndex: number; } export interface IResultSetElementKey { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts new file mode 100644 index 0000000000..e1321f2cb5 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetComplexValue } from './IResultSetComplexValue'; + +export interface IResultSetFileValue extends IResultSetComplexValue { + $type: 'file'; + fileId: string | null; + contentType?: string; + contentLength?: number; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts new file mode 100644 index 0000000000..20190e8b72 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetComplexValue } from './IResultSetComplexValue'; + +export interface IResultSetGeometryValue extends IResultSetComplexValue { + $type: 'geometry'; + srid: number; + text: string; + mapText: string | null; + properties: Record | null; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index d1bf79cb51..328ca9d696 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -43,6 +43,7 @@ export class ResultSetDataAction extends DatabaseDataAction implements IResultSetDataContentAction { static dataFormat = [ResultDataFormat.Resultset]; - private readonly view: ResultSetViewAction; - private readonly data: ResultSetDataAction; - - private readonly graphQLService: GraphQLService; - private readonly quotasService: QuotasService; - private readonly cache: Map; activeElement: IResultSetElementKey | null; constructor( source: IDatabaseDataSource, - view: ResultSetViewAction, - data: ResultSetDataAction, - graphQLService: GraphQLService, - quotasService: QuotasService, + private readonly view: ResultSetViewAction, + private readonly data: ResultSetDataAction, + private readonly format: ResultSetFormatAction, + private readonly graphQLService: GraphQLService, + private readonly quotasService: QuotasService, ) { super(source); - this.view = view; - this.data = data; - - this.graphQLService = graphQLService; - this.quotasService = quotasService; - this.cache = new Map(); this.activeElement = null; @@ -68,8 +57,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction(this, { editorData: observable, - addRows: computed, - updates: computed, set: action, add: action, addRow: action, @@ -63,6 +67,7 @@ export class ResultSetEditAction extends DatabaseEditAction { if (a.type !== b.type) { - if (a.type === DatabaseEditChangeType.update) { - return -1; - } - - if (b.type === DatabaseEditChangeType.update) { - return 1; - } - return a.type - b.type; } @@ -144,13 +141,13 @@ export class ResultSetEditAction extends DatabaseEditAction null); } - row = { ...row, key: uuid() }; + row = this.getNextRowAdd(row); if (!column) { column = this.data.getDefaultKey().column; @@ -279,6 +274,10 @@ export class ResultSetEditAction extends DatabaseEditAction> = []; - let rowIndex = 0; - let addShift = 0; - let deleteShift = 0; - - const insertedRows: IResultSetRowKey[] = []; - + applyPartialUpdate(result: IDatabaseResultSet): void { if (result.data?.rows?.length !== this.updates.length) { console.warn('ResultSetEditAction: returned data differs from performed update'); } - for (const update of this.updates) { - switch (update.type) { + const applyUpdate: Array> = []; + + const tempUpdates = this.updates + .map((update, i) => ({ + rowIndex: update.type === DatabaseEditChangeType.delete ? -1 : i, + update, + })) + .sort((a, b) => compareResultSetRowKeys(b.update.row, a.update.row)); + + let offset = tempUpdates.reduce((offset, { update }) => { + if (update.type === DatabaseEditChangeType.add) { + return offset + 1; + } + if (update.type === DatabaseEditChangeType.delete) { + return offset - 1; + } + return offset; + }, 0); + + for (const update of tempUpdates) { + const value = result.data?.rows?.[update.rowIndex]; + const row = update.update.row; + const type = update.update.type; + + switch (update.update.type) { case DatabaseEditChangeType.update: { - const value = result.data?.rows?.[rowIndex]; - - if (value !== undefined) { - this.data.setRowValue(update.row, value); - applyUpdate.push({ - type: DatabaseEditChangeType.update, - row: update.row, - newRow: update.row, - }); + if (value) { + this.data.setRowValue(update.update.row, value); } - - rowIndex++; + applyResultToUpdate(update.update, value); + this.shiftRow(update.update.row, offset); + this.removeEmptyUpdate(update.update); break; } case DatabaseEditChangeType.add: { - const value = result.data?.rows?.[rowIndex]; - - if (value !== undefined) { - const newRow = this.data.insertRow(update.row, value, addShift); - - if (newRow) { - applyUpdate.push({ - type: DatabaseEditChangeType.add, - row: update.row, - newRow, - }); - } + if (value) { + this.data.insertRow(update.update.row, value, 1); } - - insertedRows.push(update.row); - rowIndex++; - addShift++; + applyResultToUpdate(update.update, value); + this.shiftRow(update.update.row, offset); + this.removeEmptyUpdate(update.update); + offset--; break; } case DatabaseEditChangeType.delete: { - const insertShift = insertedRows.filter(row => row.index <= update.row.index).length; - const newRow = this.data.removeRow(update.row, deleteShift + insertShift); - - if (newRow) { - applyUpdate.push({ - type: DatabaseEditChangeType.delete, - row: update.row, - newRow, - }); - } - - deleteShift--; + this.revert({ row: update.update.row, column: { index: 0 } }); + this.data.removeRow(update.update.row); + offset++; break; } } + + applyUpdate.push({ + type, + row, + newRow: update.update.row, + }); } if (applyUpdate.length > 0) { @@ -391,6 +387,10 @@ export class ResultSetEditAction extends DatabaseEditAction { + const blobs: Array = []; - this.action.execute({ - resultId: this.result.id, - revert: true, - }); + for (const update of this.updates) { + if (update.type === DatabaseEditChangeType.delete) { + continue; + } + + for (let i = 0; i < update.update.length; i++) { + const value = update.update[i]; + if (isResultSetBlobValue(value) && value.fileId === null) { + blobs.push(value); + } + } + } + + return blobs; } fillBatch(batch: UpdateResultsDataBatchMutationVariables): void { @@ -479,7 +489,11 @@ export class ResultSetEditAction extends DatabaseEditAction>((obj, value, index) => { - if (value !== update.source![index]) { + if (isResultSetBlobValue(value)) { + if (value.fileId !== null) { + obj[index] = createResultSetFileValue(value.fileId, value.contentType, value.contentLength); + } + } else if (value !== update.source![index]) { obj[index] = value; } return obj; @@ -495,7 +509,7 @@ export class ResultSetEditAction extends DatabaseEditAction { + if (isResultSetBlobValue(value)) { + return null; + } + return value; + }); +} + +function replaceUploadBlobs(values: IResultSetValue[]) { + return values.map(value => { + if (isResultSetBlobValue(value)) { + if (value.fileId !== null) { + return createResultSetFileValue(value.fileId, value.contentType, value.contentLength); + } else { + return null; + } + } + return value; + }); +} + +function applyResultToUpdate(update: IResultSetUpdate, result?: IResultSetValue[]): void { + if (result) { + update.source = result; + + update.update = update.update.map((value, i) => { + const source = update.source![i]; + if (isResultSetContentValue(source) && isResultSetFileValue(value)) { + if (value.fileId && value.contentLength === source.contentLength) { + return JSON.parse(JSON.stringify(source)); + } + } + return value; + }); + } + + if (update.type === DatabaseEditChangeType.add) { + update.type = DatabaseEditChangeType.update; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts index 5ec3c69ac3..412ccd4867 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { removeLineBreak } from '@cloudbeaver/core-utils'; import { DatabaseDataAction } from '../../DatabaseDataAction'; import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; @@ -14,12 +13,25 @@ import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; import { databaseDataAction } from '../DatabaseDataActionDecorator'; import { DatabaseEditChangeType } from '../IDatabaseDataEditAction'; import type { IDatabaseDataFormatAction } from '../IDatabaseDataFormatAction'; +import type { IResultSetComplexValue } from './IResultSetComplexValue'; import type { IResultSetElementKey, IResultSetPartialKey } from './IResultSetDataKey'; +import { isResultSetBlobValue } from './isResultSetBlobValue'; +import { isResultSetComplexValue } from './isResultSetComplexValue'; import { isResultSetContentValue } from './isResultSetContentValue'; +import { isResultSetFileValue } from './isResultSetFileValue'; +import { isResultSetGeometryValue } from './isResultSetGeometryValue'; import { ResultSetEditAction } from './ResultSetEditAction'; import { ResultSetViewAction } from './ResultSetViewAction'; -export type IResultSetValue = string | number | boolean | Record | null> | null; +export type IResultSetValue = + | string + | number + | boolean + | Record | null> + | IResultSetComplexValue + | null; + +const DISPLAY_STRING_LENGTH = 200; @databaseDataAction() export class ResultSetFormatAction @@ -37,29 +49,6 @@ export class ResultSetFormatAction this.edit = edit; } - getHeaders(): string[] { - return this.view.columns.map(column => column.name!).filter(name => name !== undefined); - } - - getLongestCells(offset = 0, count?: number): string[] { - const rows = this.view.rows.slice(offset, count); - const cells: string[] = []; - - for (const row of rows) { - for (let i = 0; i < row.length; i++) { - const value = this.toDisplayString(row[i]); - const columnIndex = this.view.columnIndex({ index: i }); - const current = cells[columnIndex] ?? ''; - - if (value.length > current.length) { - cells[columnIndex] = value; - } - } - } - - return cells; - } - isReadOnly(key: IResultSetPartialKey): boolean { let readonly = false; @@ -85,26 +74,99 @@ export class ResultSetFormatAction return readonly; } + isNull(key: IResultSetElementKey): boolean { + return this.get(key) === null; + } + + isBinary(key: IResultSetPartialKey): boolean { + if (!key.column) { + return false; + } + + const column = this.view.getColumn(key.column); + if (column?.dataKind?.toLocaleLowerCase() === 'binary') { + return true; + } + + if (key.row) { + const value = this.get(key as IResultSetElementKey); + + if (isResultSetFileValue(value)) { + return true; + } + + if (isResultSetContentValue(value)) { + return value.binary !== undefined; + } + } + + return false; + } - isNull(value: IResultSetValue): boolean { - return this.get(value) === null; + getHeaders(): string[] { + return this.view.columns.map(column => column.name!).filter(name => name !== undefined); } - get(value: IResultSetValue): IResultSetValue { - if (value !== null && typeof value === 'object') { - if ('text' in value) { - return value.text; - } else if ('value' in value) { - return value.value; + getLongestCells(offset = 0, count?: number): string[] { + const cells: string[] = []; + const columnsCount = this.view.columnKeys.length; + count ??= this.view.rowKeys.length; + + for (let rowIndex = offset; rowIndex < offset + count; rowIndex++) { + for (let columnIndex = 0; columnIndex < columnsCount; columnIndex++) { + const key = { row: this.view.rowKeys[rowIndex], column: this.view.columnKeys[columnIndex] }; + const displayString = this.getDisplayString(key); + const current = cells[columnIndex] ?? ''; + + if (displayString.length > current.length) { + cells[columnIndex] = displayString; + } } - return value; } - return value; + return cells; } - getText(value: IResultSetValue): string | null { - value = this.get(value); + get(key: IResultSetElementKey): IResultSetValue { + return this.view.getCellValue(key); + } + + getText(key: IResultSetElementKey): string { + const value = this.get(key); + + if (value === null) { + return ''; + } + + if (isResultSetContentValue(value)) { + if (value.text !== undefined) { + return value.text; + } + + return ''; + } + + if (isResultSetGeometryValue(value)) { + if (value.text !== undefined) { + return value.text; + } + + return ''; + } + + if (isResultSetComplexValue(value)) { + if (value.value !== undefined) { + if (typeof value.value === 'object' && value.value !== null) { + return JSON.stringify(value.value); + } + return String(value.value); + } + return ''; + } + + if (this.isBinary(key)) { + return ''; + } if (value !== null && typeof value === 'object') { return JSON.stringify(value); @@ -117,22 +179,51 @@ export class ResultSetFormatAction return value; } - toDisplayString(value: IResultSetValue): string { - value = this.getText(value); + getDisplayString(key: IResultSetElementKey): string { + const value = this.get(key); if (value === null) { return '[null]'; } - if (typeof value === 'string' && value.length > 1000) { - return removeLineBreak( - value - .split('') - .map(v => (v.charCodeAt(0) < 32 ? ' ' : v)) - .join(''), - ); + if (isResultSetGeometryValue(value)) { + if (value.text !== undefined) { + return this.truncateText(String(value.text), DISPLAY_STRING_LENGTH); + } + + return '[null]'; + } + + if (this.isBinary(key)) { + return '[blob]'; + } + + if (isResultSetContentValue(value)) { + if (value.text !== undefined) { + return this.truncateText(String(value.text), DISPLAY_STRING_LENGTH); + } + + return '[null]'; + } + + if (isResultSetComplexValue(value)) { + if (value.value !== undefined) { + if (typeof value.value === 'object' && value.value !== null) { + return JSON.stringify(value.value); + } + return String(value.value); + } + return '[null]'; } - return removeLineBreak(String(value)); + return this.truncateText(String(value), DISPLAY_STRING_LENGTH); + } + + truncateText(text: string, length: number): string { + return text + .slice(0, length) + .split('') + .map(v => (v.charCodeAt(0) < 32 ? ' ' : v)) + .join(''); } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts index edf87178d5..e03c572cea 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts @@ -267,7 +267,11 @@ export class ResultSetSelectAction extends DatabaseSelectAction ({ index }))].sort((a, b) => a.index - b.index); + return [...this.editor.addRows, ...this.data.rows.map((c, index) => ({ index, subIndex: 0 }))].sort(compareResultSetRowKeys); } get columnKeys(): IResultSetColumnKey[] { @@ -52,10 +53,6 @@ export class ResultSetViewAction extends DatabaseDataAction(this, { - rowKeys: computed, - columnKeys: computed, - rows: computed, - columns: computed, columnsOrder: observable, setColumnOrder: action, }); @@ -129,7 +126,7 @@ export class ResultSetViewAction extends DatabaseDataAction= this.rows.length || cell.column.index >= this.columns.length) { - return undefined; + throw new Error('Cell is out of range'); } return this.rows[cell.row.index][cell.column.index]; } - getContent(cell: IResultSetElementKey): IResultSetContentValue | null { + getContent(cell: IResultSetElementKey): IResultSetComplexValue | null { const value = this.getCellValue(cell); if (isResultSetContentValue(value)) { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts new file mode 100644 index 0000000000..b4cdb97318 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetRowKey } from './IResultSetDataKey'; + +export function compareResultSetRowKeys(a: IResultSetRowKey, b: IResultSetRowKey): number { + // subIndex is used to sort rows with the same index + return a.index + a.subIndex / 10 - b.index - b.subIndex / 10; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts new file mode 100644 index 0000000000..b79fb4df77 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { createResultSetFileValue } from './createResultSetFileValue'; +import type { IResultSetBlobValue } from './IResultSetBlobValue'; + +export function createResultSetBlobValue(blob: Blob, fileId?: string): IResultSetBlobValue { + return { + ...createResultSetFileValue(fileId ?? null, blob.type, blob.size), + blob, + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts new file mode 100644 index 0000000000..27461c4df3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetContentValue } from './IResultSetContentValue'; + +export function createResultSetContentValue(data: Omit): IResultSetContentValue { + return { + $type: 'content', + ...data, + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts new file mode 100644 index 0000000000..8fa5fb8eda --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetFileValue } from './IResultSetFileValue'; + +export function createResultSetFileValue(fileId: string | null, contentType?: string, contentLength?: number): IResultSetFileValue { + return { + $type: 'file', + fileId, + contentType, + contentLength, + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts new file mode 100644 index 0000000000..c44c186315 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetBlobValue } from './IResultSetBlobValue'; +import { isResultSetFileValue } from './isResultSetFileValue'; + +export function isResultSetBlobValue(value: any): value is IResultSetBlobValue { + return isResultSetFileValue(value) && 'blob' in value && value.blob instanceof Blob; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts new file mode 100644 index 0000000000..c305b7e20b --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetComplexValue } from './IResultSetComplexValue'; + +export function isResultSetComplexValue(value: any): value is IResultSetComplexValue { + return value !== null && typeof value === 'object' && '$type' in value && typeof value.$type === 'string'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts index 38303410d7..bd0d1455c4 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts @@ -6,7 +6,8 @@ * you may not use this file except in compliance with the License. */ import type { IResultSetContentValue } from './IResultSetContentValue'; +import { isResultSetComplexValue } from './isResultSetComplexValue'; export function isResultSetContentValue(value: any): value is IResultSetContentValue { - return value !== null && typeof value === 'object' && '$type' in value && value.$type === 'content'; + return isResultSetComplexValue(value) && value.$type === 'content'; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts new file mode 100644 index 0000000000..699ef27923 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetFileValue } from './IResultSetFileValue'; +import { isResultSetComplexValue } from './isResultSetComplexValue'; + +export function isResultSetFileValue(value: any): value is IResultSetFileValue { + return isResultSetComplexValue(value) && value.$type === 'file'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts new file mode 100644 index 0000000000..9593b50ae9 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { IResultSetGeometryValue } from './IResultSetGeometryValue'; +import { isResultSetComplexValue } from './isResultSetComplexValue'; + +export function isResultSetGeometryValue(value: any): value is IResultSetGeometryValue { + return isResultSetComplexValue(value) && value.$type === 'geometry'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts index 2de6937126..a991001d02 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts @@ -7,6 +7,7 @@ */ import { action, makeObservable, runInAction } from 'mobx'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; import { getDependingDataActions } from './Actions/DatabaseDataActionDecorator'; @@ -40,13 +41,13 @@ export class DatabaseDataActions } get>(result: TResult, Action: IDatabaseDataActionClass): T { - if (Action.dataFormat && !Action.dataFormat.includes(result.dataFormat)) { + if (!isActionSupportsFormat(Action, result.dataFormat)) { throw new Error('DataFormat unsupported'); } const actions = this.actions.get(result.uniqueResultId); - let action = actions.find(action => action instanceof Action); + let action = actions.find(action => action instanceof Action && isActionSupportsFormat(action, result.dataFormat)); if (!action) { runInAction(() => { @@ -56,7 +57,7 @@ export class DatabaseDataActions for (const dependency of allDeps) { if (isDatabaseDataAction(dependency)) { - depends.push(this.get>(result, dependency)); + depends.push(this.get(result, dependency)); } else { depends.push(this.source.serviceInjector.getServiceByClass(dependency as any)); } @@ -68,7 +69,7 @@ export class DatabaseDataActions action = new Action(this.source, ...depends); action.updateResult(result, this.source.results.indexOf(result)); - this.actions.set(result.uniqueResultId, [...actions, action]); + this.actions.set(result.uniqueResultId, [...this.actions.get(result.uniqueResultId), action]); }); } @@ -80,7 +81,7 @@ export class DatabaseDataActions Action: IDatabaseDataActionInterface, ): T | undefined { const actions = this.actions.get(result.uniqueResultId); - const action = actions?.find(action => action instanceof Action); + const action = actions?.find(action => action instanceof Action && isActionSupportsFormat(action, result.dataFormat)); return action as T | undefined; } @@ -92,11 +93,6 @@ export class DatabaseDataActions const result = results.find(result => result.uniqueResultId === key); for (const action of actions) { - if (!(action.constructor as any).dataFormat.includes(result?.dataFormat)) { - this.actions.delete(key); - continue; - } - action.updateResults(results); if (!result) { @@ -120,3 +116,14 @@ export class DatabaseDataActions } } } + +function isActionSupportsFormat( + action: IDatabaseDataActionClass> | IDatabaseDataAction, + format: ResultDataFormat, +): boolean { + if ('dataFormat' in action) { + return !action.dataFormat || action.dataFormat.includes(format); + } + const constructor = action.constructor as IDatabaseDataActionClass>; + return !constructor.dataFormat || constructor.dataFormat.includes(format); +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index ea1ad375cc..3fe63db3be 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -230,7 +230,7 @@ export class TableFooterMenuService { return !editor?.isEdited(); }, - onClick: context => context.data.model.save(), + onClick: context => context.data.model.save().catch(() => {}), }); this.registerMenuItem({ diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css new file mode 100644 index 0000000000..26ae7b051a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.img { + margin: auto; + max-width: 100%; + max-height: 100%; + object-fit: contain; + + &:not(.stretch) { + flex: 0; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index 223fb7c16e..f2d40524a8 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -7,85 +7,76 @@ */ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; -import { Button, IconOrImage, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { ActionIconButtonStyles, Button, Container, Fill, IconButton, s, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { selectFiles } from '@cloudbeaver/core-browser'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { QuotasService } from '@cloudbeaver/core-root'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import { bytesToSize, download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; -import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; +import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +import { isResultSetFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetFileValue'; import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; import { ResultSetDataKeysUtils } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; -import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { QuotaPlaceholder } from '../QuotaPlaceholder'; -import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES'; - -const styles = css` - img { - margin: auto; - max-width: 100%; - max-height: 100%; - object-fit: contain; - - &[|stretch] { - margin: unset; - } - } - - container { - display: flex; - gap: 16px; - flex: 1; - flex-direction: column; - } - - image { - flex: 1; - display: flex; - overflow: auto; - } -`; +import styles from './ImageValuePresentation.m.css'; interface IToolsProps { loading?: boolean; stretch?: boolean; onToggleStretch?: () => void; onSave?: () => void; + onUpload?: () => void; } -const Tools = observer(function Tools({ loading, stretch, onToggleStretch, onSave }) { +const Tools = observer(function Tools({ loading, stretch, onToggleStretch, onSave, onUpload }) { const translate = useTranslate(); - return styled(VALUE_PANEL_TOOLS_STYLES)( - - {onSave && ( - - )} + return ( + + + {onSave && ( + + )} + {onUpload && ( + + )} + + {onToggleStretch && ( - - - - - + - - - + /> + )} - , + ); }); @@ -94,29 +85,39 @@ export const ImageValuePresentation: TabContainerPanelComponent ({ + get editAction(): ResultSetEditAction { + return this.model.source.getAction(this.resultIndex, ResultSetEditAction); + }, + get contentAction(): ResultSetDataContentAction { + return this.model.source.getAction(this.resultIndex, ResultSetDataContentAction); + }, + get selectAction(): ResultSetSelectAction { + return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); + }, + get formatAction(): ResultSetFormatAction { + return this.model.source.getAction(this.resultIndex, ResultSetFormatAction); + }, get selectedCell() { - const selection = this.model.source.getAction(this.resultIndex, ResultSetSelectAction); - const focusCell = selection.getFocusedElement(); + const focusCell = this.selectAction.getFocusedElement(); - return selection.elements[0] || focusCell; + return this.selectAction.elements[0] || focusCell; }, get cellValue() { - const view = this.model.source.getAction(this.resultIndex, ResultSetViewAction); - const cellValue = view.getCellValue(this.selectedCell); - - return cellValue; + return this.formatAction.get(this.selectedCell); }, get src() { if (this.savedSrc) { return this.savedSrc; } + if (isResultSetBlobValue(this.cellValue)) { + return URL.createObjectURL(this.cellValue.blob); + } + if (isResultSetContentValue(this.cellValue) && this.cellValue.binary) { return `data:${getMIME(this.cellValue.binary)};base64,${this.cellValue.binary}`; } else if (typeof this.cellValue === 'string' && isValidUrl(this.cellValue) && isImageFormat(this.cellValue)) { @@ -126,17 +127,28 @@ export const ImageValuePresentation: TabContainerPanelComponent { + const file = files?.item(0) ?? undefined; + if (file) { + this.editAction.set(this.selectedCell, createResultSetBlobValue(file)); + } + }); + }, }), { + editAction: computed, + contentAction: computed, + selectAction: computed, + formatAction: computed, selectedCell: computed, cellValue: computed, + canUpload: computed, src: computed, savedSrc: computed, canSave: computed, @@ -166,50 +191,58 @@ export const ImageValuePresentation: TabContainerPanelComponent { try { - await content.resolveFileDataUrl(state.selectedCell); + await state.contentAction.resolveFileDataUrl(state.selectedCell); } catch (exception: any) { notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); } }; - return styled(style)( - - - {content.isDownloadable(state.selectedCell) && ( - - )} - - - , + return ( + + + + {state.contentAction.isDownloadable(state.selectedCell) && ( + + )} + + + + ); } - return styled(style)( - - - - - - , + return ( + + + + + + ); }, ); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts index eccdc1a70d..3f7658f7cc 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts @@ -9,7 +9,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; -import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; +import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; @@ -46,7 +46,7 @@ export class ImageValuePresentationBootstrap extends Bootstrap { const cellValue = view.getCellValue(firstSelectedCell); - return !(this.isImageUrl(cellValue) || (isResultSetContentValue(cellValue) && this.isImage(cellValue))); + return !this.isImage(cellValue); } return true; @@ -56,15 +56,14 @@ export class ImageValuePresentationBootstrap extends Bootstrap { load(): void {} - private isImage(value: IResultSetContentValue | null) { - if (value !== null && 'binary' in value) { + private isImage(value: IResultSetValue) { + if (isResultSetContentValue(value) && value?.binary) { return getMIME(value.binary || '') !== null; } + if (isResultSetContentValue(value) || isResultSetBlobValue(value)) { + return value?.contentType?.startsWith('image/') ?? false; + } - return false; - } - - private isImageUrl(value: IResultSetValue | undefined) { if (typeof value !== 'string') { return false; } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx index 1cbed86c21..a7bc46d9c6 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -115,15 +115,13 @@ export const TextValuePresentation: TabContainerPanelComponent 0 || focusCell) { - const view = model.source.getAction(resultIndex, ResultSetViewAction); const format = model.source.getAction(resultIndex, ResultSetFormatAction); firstSelectedCell = selection.elements[0] || focusCell; - const value = view.getCellValue(firstSelectedCell) ?? ''; - - stringValue = format.getText(value) ?? ''; - readonly = format.isReadOnly(firstSelectedCell); + const value = format.get(firstSelectedCell); + stringValue = format.getText(firstSelectedCell); + readonly = format.isReadOnly(firstSelectedCell) || format.isBinary(firstSelectedCell); if (isResultSetContentValue(value)) { valueTruncated = content.isContentTruncated(value); diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index cdc7eff691..4ddeac3423 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -7,9 +7,21 @@ export * from './DatabaseDataModel/Actions/Document/IDocumentElementKey'; export * from './DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY'; export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM'; export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX'; +export * from './DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys'; +export * from './DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue'; +export * from './DatabaseDataModel/Actions/ResultSet/createResultSetContentValue'; +export * from './DatabaseDataModel/Actions/ResultSet/createResultSetFileValue'; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetFileValue'; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue'; export * from './DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetFileValue'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils'; diff --git a/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts index a7cd711f43..ad9e8839df 100644 --- a/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts +++ b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts @@ -5,12 +5,10 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IDatabaseDataAction, IDatabaseDataResult, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; - -import type { IGISType } from './ResultSetGISAction'; +import type { IDatabaseDataAction, IDatabaseDataResult, IResultSetElementKey, IResultSetGeometryValue } from '@cloudbeaver/plugin-data-viewer'; export interface IDatabaseDataGISAction extends IDatabaseDataAction { getGISDataFor: (selectedCells: IResultSetElementKey[]) => IResultSetElementKey[]; - getCellValue: (cell: IResultSetElementKey) => IGISType | undefined; + getCellValue: (cell: IResultSetElementKey) => IResultSetGeometryValue | undefined; isGISFormat: (cell: IResultSetElementKey) => boolean; } diff --git a/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts index 610e3303fd..2fbbe59d49 100644 --- a/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts +++ b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts @@ -12,25 +12,18 @@ import { type IDatabaseDataSource, type IDatabaseResultSet, IResultSetElementKey, + IResultSetGeometryValue, + isResultSetGeometryValue, ResultSetViewAction, } from '@cloudbeaver/plugin-data-viewer'; import type { IDatabaseDataGISAction } from './IDatabaseDataGISAction'; -export interface IGISType { - $type: string; - srid: number; - text: string; - mapText: string | null; - properties: Record | null; -} @databaseDataAction() export class ResultSetGISAction extends DatabaseDataAction implements IDatabaseDataGISAction { - private readonly GISValueType = 'geometry'; - static dataFormat = [ResultDataFormat.Resultset]; private readonly view: ResultSetViewAction; @@ -43,22 +36,20 @@ export class ResultSetGISAction isGISFormat(cell: IResultSetElementKey): boolean { const value = this.view.getCellValue(cell); - if (value !== null && typeof value === 'object' && '$type' in value) { - return value.$type === this.GISValueType; - } - - return false; + return isResultSetGeometryValue(value); } getGISDataFor(cells: IResultSetElementKey[]): IResultSetElementKey[] { return cells.filter(cell => this.isGISFormat(cell)); } - getCellValue(cell: IResultSetElementKey): IGISType | undefined { - if (!this.isGISFormat(cell)) { + getCellValue(cell: IResultSetElementKey): IResultSetGeometryValue | undefined { + const value = this.view.getCellValue(cell); + + if (!isResultSetGeometryValue(value)) { return undefined; } - return this.view.getCellValue(cell) as any as IGISType; + return value; } } diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 95355bff36..1c9b5ef903 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -18,12 +18,14 @@ import { SqlQueryResults, UpdateResultsDataBatchMutationVariables, } from '@cloudbeaver/core-sdk'; +import { uuid } from '@cloudbeaver/core-utils'; import { DatabaseDataSource, DocumentEditAction, IDatabaseDataOptions, IDatabaseResultSet, IRequestInfo, + IResultSetBlobValue, ResultSetEditAction, } from '@cloudbeaver/plugin-data-viewer'; @@ -99,31 +101,42 @@ export class QueryDataSource newResult.id === result.id, @@ -133,6 +146,13 @@ export class QueryDataSource>(funct extensions.set(...sqlDialect); const apply = async () => { - await payload.model.save(); - rejectDialog(); + try { + await payload.model.save(); + rejectDialog(); + } catch {} }; return ( From 739a6f7f1194ccb117955acc8415b2bfcc451311 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Oct 2023 23:06:07 +0200 Subject: [PATCH 04/18] chore: update version 23.2.3 (#2066) --- webapp/packages/product-default/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index 80854c1325..e8f7b68a54 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -5,7 +5,7 @@ "src/**/*.scss", "public/**/*" ], - "version": "23.2.2", + "version": "23.2.3", "description": "CloudBeaver Community", "license": "Apache-2.0", "main": "dist/index.js", From e818f1658c5b6c23b59208380d91558040755750 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Tue, 17 Oct 2023 09:32:43 +0200 Subject: [PATCH 05/18] dbeaver/pro#1905 UTF-8 is the default file encoding (#2060) --- deploy/scripts/run-server.bat | 2 +- deploy/scripts/run-server.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/scripts/run-server.bat b/deploy/scripts/run-server.bat index 9989e947e0..82d3ba3565 100644 --- a/deploy/scripts/run-server.bat +++ b/deploy/scripts/run-server.bat @@ -10,4 +10,4 @@ IF NOT EXIST workspace\.metadata ( ) ) -java %JAVA_OPTS% --add-modules=ALL-SYSTEM --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm=ALL-UNNAMED --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.security.ssl=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED --add-opens=java.base/sun.security.util=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -jar %launcherJar% -product io.cloudbeaver.product.ce.product -web-config conf/cloudbeaver.conf -nl en -registryMultiLanguage +java %JAVA_OPTS% -Dfile.encoding=UTF-8 --add-modules=ALL-SYSTEM --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm=ALL-UNNAMED --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.security.ssl=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED --add-opens=java.base/sun.security.util=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -jar %launcherJar% -product io.cloudbeaver.product.ce.product -web-config conf/cloudbeaver.conf -nl en -registryMultiLanguage diff --git a/deploy/scripts/run-server.sh b/deploy/scripts/run-server.sh index c15d98983a..b826ef55a6 100755 --- a/deploy/scripts/run-server.sh +++ b/deploy/scripts/run-server.sh @@ -6,4 +6,4 @@ echo "Starting Cloudbeaver Server" [ ! -d "workspace/.metadata" ] && mkdir -p workspace/.metadata && mkdir -p workspace/GlobalConfiguration/.dbeaver && [ ! -f "workspace/GlobalConfiguration/.dbeaver/data-sources.json" ] && cp conf/initial-data-sources.conf workspace/GlobalConfiguration/.dbeaver/data-sources.json -java ${JAVA_OPTS} --add-modules=ALL-SYSTEM --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm=ALL-UNNAMED --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.security.ssl=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED --add-opens=java.base/sun.security.util=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -jar ${launcherJar} -product io.cloudbeaver.product.ce.product -web-config conf/cloudbeaver.conf -nl en -registryMultiLanguage +java ${JAVA_OPTS} -Dfile.encoding=UTF-8 --add-modules=ALL-SYSTEM --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm=ALL-UNNAMED --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.security.ssl=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED --add-opens=java.base/sun.security.util=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -jar ${launcherJar} -product io.cloudbeaver.product.ce.product -web-config conf/cloudbeaver.conf -nl en -registryMultiLanguage From 3831abc880a7b125c232526f9fd2147ee14a49dc Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:34:52 +0200 Subject: [PATCH 06/18] CB-4038 add action icon button (#2064) * CB-4038 add action icon button * CB-4038 update styles in image value panel * CB-4038 keep default styles export --------- Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> --- .../core-blocks/src/ActionIconButton.tsx | 19 +++++++++++++ .../packages/core-blocks/src/IconButton.tsx | 10 ++++--- .../src/Menu/ACTION_ICON_BUTTON_STYLES.ts | 21 --------------- webapp/packages/core-blocks/src/index.ts | 4 +-- .../ImageValue/ImageValuePresentation.tsx | 27 +++---------------- .../FiltersTableItem.tsx | 6 ++--- .../ElementsTreeTools/ElementsTreeTools.tsx | 13 +++++---- 7 files changed, 41 insertions(+), 59 deletions(-) create mode 100644 webapp/packages/core-blocks/src/ActionIconButton.tsx delete mode 100644 webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts diff --git a/webapp/packages/core-blocks/src/ActionIconButton.tsx b/webapp/packages/core-blocks/src/ActionIconButton.tsx new file mode 100644 index 0000000000..073ee9f1cb --- /dev/null +++ b/webapp/packages/core-blocks/src/ActionIconButton.tsx @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { observer } from 'mobx-react-lite'; + +import style from './ActionIconButton.m.css'; +import { IconButton, type IconButtonProps } from './IconButton'; +import { s } from './s'; +import { useS } from './useS'; + +export const ActionIconButton: React.FC = observer(function ActionIconButton(props) { + const styles = useS(style); + + return ; +}); diff --git a/webapp/packages/core-blocks/src/IconButton.tsx b/webapp/packages/core-blocks/src/IconButton.tsx index 10f8b5f333..1158f6d740 100644 --- a/webapp/packages/core-blocks/src/IconButton.tsx +++ b/webapp/packages/core-blocks/src/IconButton.tsx @@ -5,17 +5,19 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; +import type React from 'react'; import { Button, ButtonProps } from 'reakit/Button'; import styled from 'reshadow'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { Icon } from './Icon'; +import IconButtonStyles from './IconButton.m.css'; import { s } from './s'; import { StaticImage } from './StaticImage'; import { useS } from './useS'; import { useStyles } from './useStyles'; -import IconButtonStyles from './IconButton.m.css'; interface Props { name: string; @@ -24,7 +26,9 @@ interface Props { style?: ComponentStyle; } -export function IconButton({ name, img, viewBox, style, className, ...rest }: Props & ButtonProps) { +export type IconButtonProps = Props & ButtonProps; + +export const IconButton: React.FC = observer(function IconButton({ name, img, viewBox, style, className, ...rest }) { const styles = useS(IconButtonStyles); return styled(useStyles(style))( @@ -33,4 +37,4 @@ export function IconButton({ name, img, viewBox, style, className, ...rest }: Pr {!img && } , ); -} +}); diff --git a/webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts b/webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts deleted file mode 100644 index 05180fdf80..0000000000 --- a/webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 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 { css } from 'reshadow'; - -export const ACTION_ICON_BUTTON_STYLES = css` - IconButton { - composes: theme-form-element-radius theme-ripple from global; - - padding: 4px !important; - margin: 2px !important; - width: 24px !important; - height: 24px !important; - overflow: hidden; - flex-shrink: 0; - } -`; diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 9918ccd680..f7f567b2b9 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -35,8 +35,6 @@ export * from './localization/useTranslate'; export * from './ConnectionImageWithMask/ConnectionImageWithMask'; export { default as ConnectionImageWithMaskSvgStyles } from './ConnectionImageWithMask/ConnectionImageWithMaskSvg.m.css'; -export * from './Menu/ACTION_ICON_BUTTON_STYLES'; -export { default as ActionIconButtonStyles } from './ActionIconButton.m.css'; export * from './Menu/Menu'; export { default as MenuStyles } from './Menu/Menu.m.css'; export * from './Menu/MenuBarSmallItem'; @@ -182,7 +180,9 @@ export * from './ExceptionMessage'; export { default as ExceptionMessageStyles } from './ExceptionMessage.m.css'; export * from './getComputed'; export * from './IconButton'; +export * from './ActionIconButton'; export { default as IconButtonStyles } from './IconButton.m.css'; +export { default as ActionIconButtonStyles } from './ActionIconButton.m.css'; export * from './IconOrImage'; export * from './s'; export * from './SContext'; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index f2d40524a8..fc1cee6b42 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -8,7 +8,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { ActionIconButtonStyles, Button, Container, Fill, IconButton, s, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { ActionIconButton, Button, Container, Fill, s, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { selectFiles } from '@cloudbeaver/core-browser'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -44,33 +44,14 @@ const Tools = observer(function Tools({ loading, stretch, onToggleS return ( - {onSave && ( - - )} - {onUpload && ( - - )} + {onSave && } + {onUpload && } {onToggleStretch && ( - diff --git a/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx b/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx index c316e6b100..be6f80e702 100644 --- a/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx +++ b/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { ActionIconButtonStyles, IconButton, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks'; +import { ActionIconButton, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks'; import styles from './FiltersTableItem.m.css'; @@ -20,13 +20,13 @@ interface Props { } export const FiltersTableItem = observer(function FiltersTableItem({ id, name, disabled, className, onDelete }) { - const style = useS(ActionIconButtonStyles, styles); + const style = useS(styles); return ( {name} - onDelete(id)} /> + onDelete(id)} /> ); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx index aa5350864b..90099bae30 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx @@ -10,9 +10,8 @@ import React, { useState } from 'react'; import styled from 'reshadow'; import { - ActionIconButtonStyles, + ActionIconButton, Fill, - IconButton, IconButtonStyles, PlaceholderElement, s, @@ -56,7 +55,7 @@ export const ElementsTreeTools = observer>(functi const translate = useTranslate(); const [opened, setOpen] = useState(false); const deprecatedStyles = useStyles(style); - const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles, ActionIconButtonStyles); + const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles); useCaptureViewContext(context => { context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot); @@ -70,21 +69,21 @@ export const ElementsTreeTools = observer>(functi
{tree.settings?.configurable && ( - setOpen(!opened)} /> )} - tree.refresh(root)} /> From 245d49b8926f39b5b4c266fea16e7c1f148be5fc Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:37:23 +0200 Subject: [PATCH 07/18] CB-4098 add prompt dialog to the sql editor (#2055) * CB-4098 add prompt dialog to the sql editor * CB-4098 add api integration * CB-4046 add events * CB-4046 fixes after review * CB-3913 move AI feature to the ee --------- Co-authored-by: Ainur Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../bundles/io.cloudbeaver.server/plugin.xml | 4 ++- .../WSEventHandlerWorkspaceConfigUpdate.java | 35 +++++++++++++++++++ webapp/packages/core-utils/src/index.ts | 1 + .../core-utils/src/replaceSubstring.test.ts | 28 +++++++++++++++ .../core-utils/src/replaceSubstring.ts | 13 +++++++ .../src/SqlEditor/ISQLEditorData.ts | 1 + .../SqlEditor/SqlEditorActionsMenuBar.m.css | 5 +++ .../src/SqlEditor/useSqlEditor.ts | 1 - 8 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java create mode 100644 webapp/packages/core-utils/src/replaceSubstring.test.ts create mode 100644 webapp/packages/core-utils/src/replaceSubstring.ts diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 522dfe5427..7213197f5a 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -47,7 +47,6 @@ - @@ -73,6 +72,9 @@ + + + diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java new file mode 100644 index 0000000000..fa16b732ea --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package io.cloudbeaver.server.events; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.WorkspaceConfigEventManager; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.WSWorkspaceConfigurationChangedEvent; + +public class WSEventHandlerWorkspaceConfigUpdate extends WSDefaultEventHandler { + private static final Log log = Log.getLog(WSEventHandlerWorkspaceConfigUpdate.class); + + @Override + public void handleEvent(@NotNull WSWorkspaceConfigurationChangedEvent event) { + String configFileName = event.getConfigFilePath(); + WorkspaceConfigEventManager.fireConfigChangedEvent(configFileName); + super.handleEvent(event); + } + +} \ No newline at end of file diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index ca2a5a0ccc..9ccaa2f809 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -68,3 +68,4 @@ export * from './createLastPromiseGetter'; export * from './removeMetadataFromBase64'; export * from './renamePathName'; export * from './removeLineBreak'; +export * from './replaceSubstring'; diff --git a/webapp/packages/core-utils/src/replaceSubstring.test.ts b/webapp/packages/core-utils/src/replaceSubstring.test.ts new file mode 100644 index 0000000000..4023e86fbf --- /dev/null +++ b/webapp/packages/core-utils/src/replaceSubstring.test.ts @@ -0,0 +1,28 @@ +import { replaceSubstring } from './replaceSubstring'; + +describe('replaceSubstring', () => { + it('should replace a substring correctly', () => { + const result = replaceSubstring('Hello, world!', 7, 12, 'there'); + expect(result).toBe('Hello, there!'); + }); + + it('should handle beginIndex at the start', () => { + const result = replaceSubstring('Hello, world!', 0, 5, 'Hi'); + expect(result).toBe('Hi, world!'); + }); + + it('should handle endIndex at the end', () => { + const result = replaceSubstring('Hello, world!', 7, 13, 'everyone'); + expect(result).toBe('Hello, everyone'); + }); + + it('should handle empty replacement', () => { + const result = replaceSubstring('Hello, world!', 7, 13, ''); + expect(result).toBe('Hello, '); + }); + + it('should handle replacement longer than the substring', () => { + const result = replaceSubstring('Hello, world!', 7, 12, 'everyone out there'); + expect(result).toBe('Hello, everyone out there!'); + }); +}); diff --git a/webapp/packages/core-utils/src/replaceSubstring.ts b/webapp/packages/core-utils/src/replaceSubstring.ts new file mode 100644 index 0000000000..b3c4075655 --- /dev/null +++ b/webapp/packages/core-utils/src/replaceSubstring.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function replaceSubstring(str: string, beginIndex: number, endIndex: number, replacement: string) { + const beforeSubstring = str.slice(0, beginIndex); + const afterSubstring = str.slice(endIndex); + return beforeSubstring + replacement + afterSubstring; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts index 20b99daca7..f84a1f4ab4 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts @@ -59,6 +59,7 @@ export interface ISQLEditorData { executeScript(): Promise; switchEditing(): Promise; getHintProposals(position: number, simple: boolean): Promise; + getResolvedSegment(): Promise; executeQueryAction( segment: ISQLScriptSegment | undefined, action: (query: ISQLScriptSegment) => Promise, diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css index 6622623401..a8066255f9 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css @@ -1,3 +1,8 @@ +.sqlActions { + display: flex; + flex-direction: column; +} + .sqlActions.menuBar { height: unset; width: 32px; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 1b275724b8..0e9eccaf61 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -54,7 +54,6 @@ interface ISQLEditorDataPrivate extends ISQLEditorData { updateParserScripts(): Promise; loadDatabaseDataModels(): Promise; getExecutingQuery(script: boolean): ISQLScriptSegment | undefined; - getResolvedSegment(): Promise; getSubQuery(): ISQLScriptSegment | undefined; } From d87115d753639f3970709194939b11fa5f466212 Mon Sep 17 00:00:00 2001 From: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:42:29 +0400 Subject: [PATCH 08/18] CB-3704. Validate file name (#2065) * CB-3704. Validate file name * CB-3704. Revert imports * CB-3704. Refactor after review * CB-3704. Fixed error message if files not created. * CB-3074. Remove constant to Servlet --- .../server/events/WSDeleteTempFileHandler.java | 11 +++++++---- .../service/sql/WebSQLFileLoaderServlet.java | 8 +++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java index 40e44c9e3a..4accdf7fe6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java @@ -25,6 +25,7 @@ import org.jkiss.utils.IOUtils; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; public class WSDeleteTempFileHandler implements WSEventHandler { @@ -36,10 +37,12 @@ public void resetTempFolder(String sessionId) { Path path = CBPlatform.getInstance() .getTempFolder(new VoidProgressMonitor(), TEMP_FILE_FOLDER) .resolve(sessionId); - try { - IOUtils.deleteDirectory(path); - } catch (IOException e) { - log.error("Error deleting temp path", e); + if (Files.exists(path)) { + try { + IOUtils.deleteDirectory(path); + } catch (IOException e) { + log.error("Error deleting temp path", e); + } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java index f05434a483..f8ef14585b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -53,6 +53,8 @@ public class WebSQLFileLoaderServlet extends WebServiceServletBase { private static final String FILE_ID = "fileId"; + private static final String FORBIDDEN_CHARACTERS_FILE_REGEX = "(?U)[\\w.$()@ -]+"; + private static final Gson gson = new GsonBuilder() .serializeNulls() .setPrettyPrinting() @@ -88,7 +90,7 @@ protected void processServiceRequest( String fileId = JSONUtils.getString(variables, FILE_ID); - if (fileId != null) { + if (fileId != null && !fileId.matches(FORBIDDEN_CHARACTERS_FILE_REGEX)) { Path file = tempFolder.resolve(fileId); try { Files.write(file, request.getPart("fileData").getInputStream().readAllBytes()); @@ -96,6 +98,10 @@ protected void processServiceRequest( log.error(e.getMessage()); throw new DBWebException(e.getMessage()); } + } else { + String illegalCharacters = fileId != null ? + fileId.replaceAll(FORBIDDEN_CHARACTERS_FILE_REGEX, " ").strip() : null; + throw new DBException("Resource path '" + fileId + "' contains illegal characters: " + illegalCharacters); } } } \ No newline at end of file From d44e6b4e7adc31b736b28994b13fadc26e0670bf Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 18 Oct 2023 21:17:16 +0300 Subject: [PATCH 09/18] CB-3564 multi server license (#2062) * CB-3564 fix: localization * CB-3984 wip * CB-3984 multi instance license * CB-3564 feat: external users provider * CB-3984 server init fix * CB-3564 feat: load resource for administration users management * CB-3984 force enable imported user --------- Co-authored-by: Aleksandr Skoblikov Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> --- .vscode/launch.json | 8 +- .../io/cloudbeaver/server/CBApplication.java | 16 +- .../CBEmbeddedSecurityController.java | 152 ++++++++++------ .../EmbeddedSecurityControllerFactory.java | 6 +- .../core-blocks/src/FormControls/Field.tsx | 4 +- .../src/Placeholder/Placeholder.tsx | 11 ++ .../src/Placeholder/PlaceholderContainer.ts | 6 +- .../src/ResourcesHooks/useResource.ts | 26 ++- .../src/Resource/CachedMapResource.ts | 11 +- .../src/Resource/CachedResource.ts | 152 +--------------- .../core-resource/src/Resource/IResource.ts | 45 +++++ .../core-resource/src/Resource/Resource.ts | 164 ++++++++++++++++++ .../src/Resource/ResourceError.ts | 6 +- .../src/Resource/ResourceMetadata.ts | 21 ++- webapp/packages/core-resource/src/index.ts | 2 + .../ServerEventEmitter/TopicEventHandler.ts | 18 +- .../core-view/src/Menu/MenuService.ts | 24 +-- .../src/Administration/Administration.tsx | 8 +- .../src/Administration/ItemContent.tsx | 12 +- .../ServerConfigurationDriversForm.tsx | 3 +- .../plugin-administration/src/locales/en.ts | 4 +- .../Users/UserForm/Info/UserFormInfo.tsx | 7 +- .../Users/UsersTable/CreateUserBootstrap.ts | 11 +- .../Administration/Users/UsersTable/User.tsx | 20 ++- .../UsersAdministrationToolsPanel.tsx | 2 +- .../Users/UsersTable/UsersPage.tsx | 8 +- .../AdministrationUsersManagementService.ts | 42 +++++ .../src/externalUserProviderStatusContext.ts | 28 +++ .../src/index.ts | 2 + .../src/manifest.ts | 2 + .../plugin-devtools/src/DevToolsService.ts | 22 ++- .../plugin-devtools/src/PluginBootstrap.ts | 12 +- .../ACTION_DEVTOOLS_MODE_CONFIGURATION.ts | 14 ++ 33 files changed, 578 insertions(+), 291 deletions(-) create mode 100644 webapp/packages/core-resource/src/Resource/IResource.ts create mode 100644 webapp/packages/core-resource/src/Resource/Resource.ts create mode 100644 webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts create mode 100644 webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts create mode 100644 webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index caa67e126d..13ce3d5f43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,13 @@ "sourceMaps": true, "sourceMapPathOverrides": { "webpack:///*": "${workspaceFolder}/../*" - } + }, + "skipFiles": [ + "/**", + "**/node_modules/**", + "${workspaceFolder}/webapp/**/node_modules/**/*.js", + "${workspaceFolder}/webapp/**/dist/**/*.js" + ] }, { "type": "java", diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index c903d3b7e1..946ba17e73 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -325,14 +325,6 @@ protected void startServer() { } - { - try { - initializeSecurityController(); - } catch (Exception e) { - log.error("Error initializing database", e); - return; - } - } try { initializeServer(); } catch (DBException e) { @@ -340,6 +332,14 @@ protected void startServer() { return; } + try { + initializeSecurityController(); + } catch (Exception e) { + log.error("Error initializing database", e); + return; + } + + if (configurationMode) { // Try to configure automatically performAutoConfiguration(configPath.toFile().getParentFile()); diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index ec5407dc53..0a06993d3d 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -67,7 +67,8 @@ /** * Server controller */ -public class CBEmbeddedSecurityController implements SMAdminController, SMAuthenticationManager { +public class CBEmbeddedSecurityController + implements SMAdminController, SMAuthenticationManager { private static final Log log = Log.getLog(CBEmbeddedSecurityController.class); @@ -80,14 +81,14 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen }.getType(); private static final Gson gson = new GsonBuilder().create(); - protected final WebAuthApplication application; + protected final T application; protected final CBDatabase database; protected final SMCredentialsProvider credentialsProvider; protected final SMControllerConfiguration smConfig; public CBEmbeddedSecurityController( - WebAuthApplication application, + T application, CBDatabase database, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig @@ -132,44 +133,69 @@ public void createUser( log.debug("Create user: " + userId); try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - createAuthSubject(dbCon, userId, SUBJECT_USER); - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER" + - "(USER_ID,IS_ACTIVE,CREATE_TIME,DEFAULT_AUTH_ROLE) VALUES(?,?,?,?)")) - ) { - dbStat.setString(1, userId); - dbStat.setString(2, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); - dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); - if (CommonUtils.isEmpty(defaultAuthRole)) { - dbStat.setNull(4, Types.VARCHAR); - } else { - dbStat.setString(4, defaultAuthRole); - } - dbStat.execute(); + createUser(dbCon, userId, metaParameters, enabled, defaultAuthRole); + String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); + if (!CommonUtils.isEmpty(defaultTeamName)) { + setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId); } - saveSubjectMetas(dbCon, userId, metaParameters); txn.commit(); } - String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); - if (!CommonUtils.isEmpty(defaultTeamName)) { - setUserTeams(userId, new String[]{defaultTeamName}, userId); - } } catch (SQLException e) { throw new DBCException("Error saving user in database", e); } } + public void createUser( + @NotNull Connection dbCon, + @NotNull String userId, + @Nullable Map metaParameters, + boolean enabled, + @Nullable String defaultAuthRole + ) throws DBException, SQLException { + createAuthSubject(dbCon, userId, SUBJECT_USER); + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER" + + "(USER_ID,IS_ACTIVE,CREATE_TIME,DEFAULT_AUTH_ROLE) VALUES(?,?,?,?)")) + ) { + dbStat.setString(1, userId); + dbStat.setString(2, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + if (CommonUtils.isEmpty(defaultAuthRole)) { + dbStat.setNull(4, Types.VARCHAR); + } else { + dbStat.setString(4, defaultAuthRole); + } + dbStat.execute(); + } + saveSubjectMetas(dbCon, userId, metaParameters); + + } + @Override public void importUsers(@NotNull SMUserImportList userImportList) throws DBException { for (SMUserProvisioning user : userImportList.getUsers()) { if (isSubjectExists(user.getUserId())) { log.info("Skip already exist user: " + user.getUserId()); + setUserAuthRole(user.getUserId(), userImportList.getAuthRole()); continue; } createUser(user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); } } + protected void importUsers(@NotNull Connection connection, @NotNull SMUserImportList userImportList) + throws DBException, SQLException { + for (SMUserProvisioning user : userImportList.getUsers()) { + if (isSubjectExists(user.getUserId())) { + log.info("User already exist : " + user.getUserId()); + setUserAuthRole(connection, user.getUserId(), userImportList.getAuthRole()); + enableUser(connection, user.getUserId(), true); + continue; + } + createUser(connection, user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); + } + } + @Override public void deleteUser(String userId) throws DBCException { invalidateAllUserTokens(userId); @@ -192,25 +218,7 @@ public void deleteUser(String userId) throws DBCException { public void setUserTeams(String userId, String[] teamIds, String grantorId) throws DBCException { try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - JDBCUtils.executeStatement( - dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"), - userId - ); - if (!ArrayUtils.isEmpty(teamIds)) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" + - "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")) - ) { - for (String teamId : teamIds) { - dbStat.setString(1, userId); - dbStat.setString(2, teamId); - dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); - dbStat.setString(4, grantorId); - dbStat.execute(); - } - } - } + setUserTeams(dbCon, userId, teamIds, grantorId); txn.commit(); } } catch (SQLException e) { @@ -219,6 +227,28 @@ public void setUserTeams(String userId, String[] teamIds, String grantorId) thro addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); } + public void setUserTeams(@NotNull Connection dbCon, String userId, String[] teamIds, String grantorId) + throws SQLException { + JDBCUtils.executeStatement( + dbCon, + database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"), + userId + ); + if (!ArrayUtils.isEmpty(teamIds)) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" + + "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")) + ) { + for (String teamId : teamIds) { + dbStat.setString(1, userId); + dbStat.setString(2, teamId); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + dbStat.setString(4, grantorId); + dbStat.execute(); + } + } + } + } @NotNull @@ -563,17 +593,21 @@ public void setCurrentUserParameter(String name, Object value) throws DBExceptio public void enableUser(String userId, boolean enabled) throws DBException { try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET IS_ACTIVE=? WHERE USER_ID=?"))) { - dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); - dbStat.setString(2, userId); - dbStat.executeUpdate(); - } + enableUser(dbCon, userId, enabled); } catch (SQLException e) { throw new DBCException("Error while updating user configuration", e); } } + public void enableUser(Connection dbCon, String userId, boolean enabled) throws SQLException { + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( + "UPDATE {table_prefix}CB_USER SET IS_ACTIVE=? WHERE USER_ID=?"))) { + dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + dbStat.setString(2, userId); + dbStat.executeUpdate(); + } + } + @Override public void setUserAuthRole(@NotNull String userId, @Nullable String authRole) throws DBException { if (credentialsProvider.getActiveUserCredentials() != null @@ -582,20 +616,28 @@ public void setUserAuthRole(@NotNull String userId, @Nullable String authRole) t throw new SMException("User cannot change his own role"); } try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET DEFAULT_AUTH_ROLE=? WHERE USER_ID=?"))) { - dbStat.setString(1, authRole); - dbStat.setString(2, userId); - if (dbStat.executeUpdate() <= 0) { - throw new SMException("User not found"); - } - } + setUserAuthRole(dbCon, userId, authRole); } catch (SQLException e) { throw new DBCException("Error while updating user authentication role", e); } addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); } + public void setUserAuthRole(@NotNull Connection dbCon, @NotNull String userId, @Nullable String authRole) + throws DBException, SQLException { + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET DEFAULT_AUTH_ROLE=? WHERE USER_ID=?"))) { + if (authRole == null) { + dbStat.setNull(1, Types.VARCHAR); + } else { + dbStat.setString(1, authRole); + } + dbStat.setString(2, userId); + if (dbStat.executeUpdate() <= 0) { + throw new SMException("User not found"); + } + } + } /////////////////////////////////////////// diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java index 855cbc8c56..075bf04b46 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java @@ -31,7 +31,7 @@ /** * Embedded Security Controller Factory */ -public class EmbeddedSecurityControllerFactory { +public class EmbeddedSecurityControllerFactory { private static volatile CBDatabase DB_INSTANCE; public static CBDatabase getDbInstance() { @@ -42,7 +42,7 @@ public static CBDatabase getDbInstance() { * Create new security controller instance with custom configuration */ public CBEmbeddedSecurityController createSecurityService( - WebAuthApplication application, + T application, Map databaseConfig, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig @@ -86,7 +86,7 @@ private synchronized void initDatabase(WebAuthApplication application, Map> = observer(function Field({ children, className, ...rest }) { const styles = useS(fieldStyles, elementsSizeStyles); - const layoutProps = getLayoutProps(rest); + rest = filterLayoutFakeProps(rest); return (
diff --git a/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx b/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx index c9fe9f93aa..4b5220227e 100644 --- a/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx +++ b/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx @@ -7,6 +7,9 @@ */ import { observer } from 'mobx-react-lite'; +import { isDefined } from '@cloudbeaver/core-utils'; + +import { useAutoLoad } from '../Loader/useAutoLoad'; import type { PlaceholderContainer, PlaceholderElement } from './PlaceholderContainer'; type Props> = T & { @@ -27,6 +30,14 @@ export const Placeholder = observer(function Placeholder element.getLoaders?.(rest as unknown as T)) + .flat() + .filter(isDefined), + ); + elements = elements.filter(placeholder => !placeholder.isHidden?.(rest as unknown as T)); return ( diff --git a/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts b/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts index 8c3a93ab24..967c1c30aa 100644 --- a/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts +++ b/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts @@ -7,7 +7,7 @@ */ import { observable } from 'mobx'; -import { uuid } from '@cloudbeaver/core-utils'; +import { ILoadableState, uuid } from '@cloudbeaver/core-utils'; export type PlaceholderComponent = Record> = React.FunctionComponent; @@ -16,6 +16,7 @@ export interface PlaceholderElement = Record; isHidden?: (props: T) => boolean; order?: number; + getLoaders?: (props: T) => ILoadableState[]; } export class PlaceholderContainer = Record> { @@ -29,12 +30,13 @@ export class PlaceholderContainer = Record !placeholder.isHidden?.(props)); } - add(component: PlaceholderComponent, order?: number, isHidden?: (props: T) => boolean): void { + add(component: PlaceholderComponent, order?: number, isHidden?: (props: T) => boolean, getLoaders?: (props: T) => ILoadableState[]): void { const placeholder: PlaceholderElement = { id: uuid(), component, order, isHidden, + getLoaders, }; if (order === undefined) { diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index e98f215baa..b2baf0282c 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -17,12 +17,13 @@ import { CachedMapResourceListGetter, CachedMapResourceLoader, CachedMapResourceValue, - CachedResource, CachedResourceContext, CachedResourceData, CachedResourceKey, + IResource, isResourceKeyList, isResourceKeyListAlias, + Resource, ResourceKey, ResourceKeyList, ResourceKeyListAlias, @@ -39,16 +40,11 @@ export interface ResourceKeyWithIncludes { readonly includes: TIncludes; } -type ResourceData, TKey, TIncludes> = TResource extends CachedDataResource< - any, - any, - any, - any -> +type ResourceData, TKey, TIncludes> = TResource extends CachedDataResource ? CachedResourceData : CachedMapResourceLoader, CachedResourceData extends Map ? I : never, TIncludes>; -interface IActions, TKey, TIncludes> { +interface IActions, TKey, TIncludes> { active?: boolean; forceSuspense?: boolean; silent?: boolean; @@ -111,7 +107,7 @@ type TResult = TResource extends CachedDataResource< * @param actions */ export function useResource< - TResource extends CachedResource, + TResource extends IResource, TKeyArg extends ResourceKey>, TIncludes extends Readonly>, >( @@ -122,7 +118,7 @@ export function useResource< ): TResult; export function useResource< - TResource extends CachedResource, + TResource extends Resource, TKeyArg extends ResourceKey>, TIncludes extends CachedResourceContext, >( @@ -132,7 +128,7 @@ export function useResource< actions?: TResource extends any ? IActions : never, ): IMapResourceResult | IMapResourceListResult | IDataResourceResult { // eslint-disable-next-line react-hooks/rules-of-hooks - const resource = ctor instanceof CachedResource ? ctor : useService(ctor); + const resource = ctor instanceof Resource ? ctor : useService(ctor); const errorContext = useContext(ErrorContext); let key: ResourceKey | null = keyObj as ResourceKey; let includes: TIncludes = [] as unknown as TIncludes; @@ -233,10 +229,10 @@ export function useResource< const { key, includes, resource } = propertiesRef; if (refresh) { - resource.markOutdated(key); + await resource.refresh(key, includes as any); + } else { + await resource.load(key, includes as any); } - - await resource.load(key, includes as any); }, async load(refresh?: boolean): Promise { if (propertiesRef.key === null) { @@ -332,7 +328,7 @@ export function useResource< if (!this.isLoaded()) { if (this.loading) { - throw this.resource.waitLoad(); + throw refObj.load(); } if (this.canLoad) { diff --git a/webapp/packages/core-resource/src/Resource/CachedMapResource.ts b/webapp/packages/core-resource/src/Resource/CachedMapResource.ts index 72f4d31048..befc5a3bac 100644 --- a/webapp/packages/core-resource/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedMapResource.ts @@ -5,14 +5,15 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable } from 'mobx'; +import { action, computed, entries, keys, makeObservable, values } from 'mobx'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver/core-utils'; -import { CachedResource, CachedResourceKey } from './CachedResource'; +import { CachedResource } from './CachedResource'; import type { CachedResourceIncludeArgs, CachedResourceValueIncludes } from './CachedResourceIncludes'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { CachedResourceKey } from './IResource'; import type { ResourceKey, ResourceKeySimple } from './ResourceKey'; import type { ResourceKeyAlias } from './ResourceKeyAlias'; import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList'; @@ -46,15 +47,15 @@ export abstract class CachedMapResource< readonly onItemDelete: ISyncExecutor>; get entries(): [TKey, TValue][] { - return Array.from(this.data.entries()); + return entries(this.data) as [TKey, TValue][]; } get values(): TValue[] { - return Array.from(this.data.values()); + return values(this.data) as TValue[]; } get keys(): TKey[] { - return Array.from(this.data.keys()); + return keys(this.data) as TKey[]; } constructor(defaultValue?: () => Map, defaultIncludes?: CachedResourceIncludeArgs) { diff --git a/webapp/packages/core-resource/src/Resource/CachedResource.ts b/webapp/packages/core-resource/src/Resource/CachedResource.ts index 978b9765e7..531782251e 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResource.ts @@ -5,9 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable, observable, toJS } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; -import { Dependency } from '@cloudbeaver/core-di'; import { ExecutionContext, Executor, @@ -19,7 +18,6 @@ import { SyncExecutor, TaskScheduler, } from '@cloudbeaver/core-executor'; -import { isPrimitive, MetadataMap } from '@cloudbeaver/core-utils'; import { CachedResourceOffsetPageKey, @@ -29,30 +27,21 @@ import { isOffsetPageOutdated, } from './CachedResourceOffsetPageKeys'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { IResource } from './IResource'; +import { Resource } from './Resource'; import { isResourceAlias } from './ResourceAlias'; -import { ResourceAliases } from './ResourceAliases'; import { ResourceError } from './ResourceError'; import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; import { resourceKeyAlias } from './ResourceKeyAlias'; -import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList'; +import { resourceKeyList } from './ResourceKeyList'; import { resourceKeyListAlias } from './ResourceKeyListAlias'; -import { ResourceKeyUtils } from './ResourceKeyUtils'; -import { ResourceLogger } from './ResourceLogger'; -import { ResourceMetadata } from './ResourceMetadata'; import { ResourceOffsetPagination } from './ResourceOffsetPagination'; -import { ResourceUseTracker } from './ResourceUseTracker'; export interface IDataError { param: ResourceKey; exception: Error; } -export type CachedResourceData = TResource extends CachedResource ? T : never; -export type CachedResourceValue = TResource extends CachedResource ? T : never; -export type CachedResourceKey = TResource extends CachedResource ? T : never; -export type CachedResourceContext = TResource extends CachedResource ? T : void; -export type CachedResourceMetadata = TResource extends CachedResource ? T : void; - export const CachedResourceParamKey = resourceKeyAlias('@cached-resource/param-default'); export const CachedResourceListEmptyKey = resourceKeyListAlias('@cached-resource/empty'); @@ -65,48 +54,33 @@ export abstract class CachedResource< TKey, TInclude extends ReadonlyArray, TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, -> extends Dependency { - data: TData; - +> extends Resource { readonly onClear: ISyncExecutor; readonly onDataOutdated: ISyncExecutor>; readonly onDataUpdate: ISyncExecutor>; readonly onDataError: ISyncExecutor>>; readonly beforeLoad: IExecutor>; - readonly useTracker: ResourceUseTracker; readonly offsetPagination: ResourceOffsetPagination; - readonly aliases: ResourceAliases; - protected defaultIncludes: TInclude; protected get loading(): boolean { return this.scheduler.executing; } protected outdateWaitList: ResourceKey[]; protected readonly scheduler: TaskScheduler>; - protected readonly logger: ResourceLogger; - protected readonly metadata: ResourceMetadata; /** Need to infer value type */ private readonly typescriptHack: TValue; - constructor(defaultKey: ResourceKey, private readonly defaultValue: () => TData, defaultIncludes: TInclude = [] as any) { - super(); + constructor(defaultKey: ResourceKey, defaultValue: () => TData, defaultIncludes: TInclude = [] as any) { + super(defaultValue, defaultIncludes); - this.logger = new ResourceLogger(this.getName()); - this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this)); - this.metadata = new ResourceMetadata(this.aliases, this.getDefaultMetadata.bind(this), this.isKeyEqual.bind(this), this.getKeyRef.bind(this)); this.offsetPagination = new ResourceOffsetPagination(this.metadata); - this.useTracker = new ResourceUseTracker(this.logger, this.aliases, this.metadata); - this.isKeyEqual = this.isKeyEqual.bind(this); - this.isIntersect = this.isIntersect.bind(this); this.loadingTask = this.loadingTask.bind(this); this.typescriptHack = null as any; - this.defaultIncludes = defaultIncludes; this.outdateWaitList = []; this.scheduler = new TaskScheduler(this.isIntersect); - this.data = defaultValue(); this.beforeLoad = new Executor(null, this.isIntersect); this.onClear = new SyncExecutor(); this.onDataOutdated = new SyncExecutor>(null); @@ -124,7 +98,6 @@ export abstract class CachedResource< this.logger.spy(this.onDataError, 'onDataError'); makeObservable(this, { - data: observable, loader: action, markLoading: action, markLoaded: action, @@ -146,15 +119,11 @@ export abstract class CachedResource< }, 5 * 60 * 1000); } - getName(): string { - return this.constructor.name; - } - /** * Mark resource as in use when {@link resource} is in use * @param resource resource to depend on */ - connect(resource: CachedResource): void { + connect(resource: IResource): void { let subscription: string | null = null; const subscriptionHandler = () => { @@ -305,18 +274,7 @@ export abstract class CachedResource< } } - return this.metadata.every(param, metadata => metadata.loaded) && (!includes || this.isIncludes(param, includes)); - } - - /** - * Return true if resource is outdated or not loaded - * @param param - Resource key - */ - isLoadable(param?: ResourceKey, context?: TInclude): boolean { - if (param === undefined) { - param = CachedResourceParamKey; - } - return !this.isLoaded(param, context) || this.isOutdated(param); + return this.metadata.every(param, metadata => metadata.loaded && (!includes || includes.every(include => metadata.includes.includes(include)))); } /** @@ -327,34 +285,6 @@ export abstract class CachedResource< return this.scheduler.wait(); } - isLoading(key?: ResourceKey): boolean { - if (key === undefined) { - key = CachedResourceParamKey; - } - - return this.metadata.some(key, metadata => metadata.loading); - } - - /** - * Return true if specified {@link includes} is loaded for specified {@link key} - * @param key - Resource key - * @param includes - Includes - */ - isIncludes(key: ResourceKey, includes: TInclude): boolean { - return this.metadata.every(key, metadata => includes.every(include => metadata.includes.includes(include))); - } - - getException(param: ResourceKeyFlat): Error | null; - getException(param: ResourceKeyList): Error[] | null; - getException(param: ResourceKey): Error[] | Error | null; - getException(param: ResourceKey): Error[] | Error | null { - if (isResourceKeyList(param)) { - return this.metadata.map(param, metadata => metadata?.exception || null).filter((exception): exception is Error => exception !== null); - } - - return this.metadata.map(param, metadata => metadata?.exception || null); - } - isOutdated(param?: ResourceKey): boolean { if (param === undefined) { param = CachedResourceParamKey; @@ -564,55 +494,6 @@ export abstract class CachedResource< }, {}); } - /** - * Can be overridden to provide equality check for complicated keys - */ - isKeyEqual(param: TKey, second: TKey): boolean { - return param === second; - } - - /** - * Check if key is a part of nextKey - * @param nextKey - Resource key - * @param key - Resource key - * @returns {boolean} Returns true if key can be represented by nextKey - */ - isIntersect(key: ResourceKey, nextKey: ResourceKey): boolean { - if (key === nextKey) { - return true; - } - - if (isResourceAlias(key) && isResourceAlias(nextKey)) { - key = this.aliases.transformToAlias(key); - nextKey = this.aliases.transformToAlias(nextKey); - - return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target); - } else if (isResourceAlias(key) || isResourceAlias(nextKey)) { - return true; - } - - if (isResourceKeyList(key) || isResourceKeyList(nextKey)) { - return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); - } - - return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); - } - - /** - * Can be overridden to provide static link to complicated keys - */ - protected getKeyRef(key: TKey): TKey { - if (isPrimitive(key)) { - return key; - } - return Object.freeze(toJS(key)); - } - - /** - * Check if key is valid. Can be overridden to provide custom validation. - */ - protected abstract validateKey(key: TKey): boolean; - protected resetIncludes(): void { this.metadata.update(metadata => { metadata.includes = observable([...this.defaultIncludes]); @@ -670,21 +551,6 @@ export abstract class CachedResource< this.onDataOutdated.execute(key); } - /** - * Use to extend metadata - * @returns {Record} Object Map - */ - protected getDefaultMetadata(key: TKey, metadata: MetadataMap): TMetadata { - return { - loaded: false, - outdated: true, - loading: false, - exception: null, - includes: observable([...this.defaultIncludes]), - dependencies: observable([]), - } as ICachedResourceMetadata as TMetadata; - } - protected async preLoadData( param: ResourceKey, contexts: IExecutionContextProvider>, diff --git a/webapp/packages/core-resource/src/Resource/IResource.ts b/webapp/packages/core-resource/src/Resource/IResource.ts new file mode 100644 index 0000000000..b2ccf69be1 --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/IResource.ts @@ -0,0 +1,45 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { ResourceAliases } from './ResourceAliases'; +import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; +import type { ResourceKeyList } from './ResourceKeyList'; +import type { ResourceUseTracker } from './ResourceUseTracker'; + +export type CachedResourceData = TResource extends IResource ? T : never; +export type CachedResourceKey = TResource extends IResource ? T : never; +export type CachedResourceContext = TResource extends IResource ? T : void; +export type CachedResourceValue = TResource extends IResource ? T : never; +export type CachedResourceMetadata = TResource extends IResource ? T : void; + +export interface IResource< + TData, + TKey, + TInclude extends ReadonlyArray, + TValue = TData, + TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, +> { + data: TData; + readonly aliases: ResourceAliases; + readonly useTracker: ResourceUseTracker; + getName(): string; + + getException(param: ResourceKeyFlat): Error | null; + getException(param: ResourceKeyList): Error[] | null; + getException(param: ResourceKey): Error[] | Error | null; + getException(param: ResourceKey): Error[] | Error | null; + + isLoadable(param?: ResourceKey, context?: TInclude): boolean; + isLoaded(param?: ResourceKey, includes?: TInclude): boolean; + isLoading(key?: ResourceKey): boolean; + isOutdated(param?: ResourceKey): boolean; + isIntersect(key: ResourceKey, nextKey: ResourceKey): boolean; + + load(key?: ResourceKey, context?: TInclude): Promise; + refresh(key?: ResourceKey, context?: TInclude): Promise; +} diff --git a/webapp/packages/core-resource/src/Resource/Resource.ts b/webapp/packages/core-resource/src/Resource/Resource.ts new file mode 100644 index 0000000000..12cc9a18f6 --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/Resource.ts @@ -0,0 +1,164 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { makeObservable, observable, toJS } from 'mobx'; + +import { Dependency } from '@cloudbeaver/core-di'; +import { isContainsException, isPrimitive, MetadataMap } from '@cloudbeaver/core-utils'; + +import { CachedResourceParamKey } from './CachedResource'; +import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { IResource } from './IResource'; +import { isResourceAlias } from './ResourceAlias'; +import { ResourceAliases } from './ResourceAliases'; +import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; +import { isResourceKeyList, type ResourceKeyList } from './ResourceKeyList'; +import { ResourceKeyUtils } from './ResourceKeyUtils'; +import { ResourceLogger } from './ResourceLogger'; +import { ResourceMetadata } from './ResourceMetadata'; +import { ResourceUseTracker } from './ResourceUseTracker'; + +export abstract class Resource< + TData, + TKey, + TInclude extends ReadonlyArray, + TValue = TData, + TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, + > + extends Dependency + implements IResource +{ + data: TData; + + readonly aliases: ResourceAliases; + readonly useTracker: ResourceUseTracker; + + protected readonly logger: ResourceLogger; + protected readonly metadata: ResourceMetadata; + + constructor(protected readonly defaultValue: () => TData, protected defaultIncludes: TInclude = [] as any) { + super(); + this.isKeyEqual = this.isKeyEqual.bind(this); + this.isIntersect = this.isIntersect.bind(this); + + this.logger = new ResourceLogger(this.getName()); + this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this)); + this.metadata = new ResourceMetadata(this.aliases, this.getDefaultMetadata.bind(this), this.isKeyEqual, this.getKeyRef.bind(this)); + this.useTracker = new ResourceUseTracker(this.logger, this.aliases, this.metadata); + + this.data = this.defaultValue(); + + makeObservable(this, { + data: observable, + }); + } + + abstract isLoaded(param?: ResourceKey | undefined, includes?: TInclude | undefined): boolean; + abstract isOutdated(param?: ResourceKey | undefined): boolean; + + isLoadable(param?: ResourceKey | undefined, context?: TInclude | undefined): boolean { + if (param === undefined) { + param = CachedResourceParamKey; + } + + if (isContainsException(this.getException(param))) { + return false; + } + + return !this.isLoaded(param, context) || this.isOutdated(param); + } + + isLoading(key?: ResourceKey): boolean { + if (key === undefined) { + key = CachedResourceParamKey; + } + + return this.metadata.some(key, metadata => metadata.loading); + } + + /** + * Check if key is a part of nextKey + * @param nextKey - Resource key + * @param key - Resource key + * @returns {boolean} Returns true if key can be represented by nextKey + */ + isIntersect(key: ResourceKey, nextKey: ResourceKey): boolean { + if (key === nextKey) { + return true; + } + + if (isResourceAlias(key) && isResourceAlias(nextKey)) { + key = this.aliases.transformToAlias(key); + nextKey = this.aliases.transformToAlias(nextKey); + + return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target); + } else if (isResourceAlias(key) || isResourceAlias(nextKey)) { + return true; + } + + if (isResourceKeyList(key) || isResourceKeyList(nextKey)) { + return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); + } + + return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); + } + + /** + * Can be overridden to provide equality check for complicated keys + */ + isKeyEqual(param: TKey, second: TKey): boolean { + return param === second; + } + + getName(): string { + return this.constructor.name; + } + + getException(param: ResourceKeyFlat): Error | null; + getException(param: ResourceKeyList): Error[] | null; + getException(param: ResourceKey): Error[] | Error | null; + getException(param: ResourceKey): Error[] | Error | null { + if (isResourceKeyList(param)) { + return this.metadata.map(param, metadata => metadata?.exception || null).filter((exception): exception is Error => exception !== null); + } + + return this.metadata.map(param, metadata => metadata?.exception || null); + } + + abstract load(key?: ResourceKey | undefined, context?: TInclude | undefined): Promise; + abstract refresh(key?: ResourceKey | undefined, context?: TInclude | undefined): Promise; + + /** + * Can be overridden to provide static link to complicated keys + */ + protected getKeyRef(key: TKey): TKey { + if (isPrimitive(key)) { + return key; + } + return Object.freeze(toJS(key)); + } + + /** + * Check if key is valid. Can be overridden to provide custom validation. + */ + protected abstract validateKey(key: TKey): boolean; + + /** + * Use to extend metadata + * @returns {Record} Object Map + */ + protected getDefaultMetadata(key: TKey, metadata: MetadataMap): TMetadata { + return { + loaded: false, + outdated: true, + loading: false, + exception: null, + includes: observable([...this.defaultIncludes]), + dependencies: observable([]), + } as ICachedResourceMetadata as TMetadata; + } +} diff --git a/webapp/packages/core-resource/src/Resource/ResourceError.ts b/webapp/packages/core-resource/src/Resource/ResourceError.ts index 5f6894a5be..5dd282765d 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceError.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceError.ts @@ -5,6 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { action } from 'mobx'; + import { LoadingError } from '@cloudbeaver/core-utils'; import type { CachedResource } from './CachedResource'; @@ -18,12 +20,12 @@ export class ResourceError extends LoadingError { options?: ErrorOptions, ) { super( - () => { + action(() => { // @TODO extract clean error logic to the CachedResource. // For now when the ResourceError is thrown and refresh fn is called, the error is not cleaned in the resource this.resource.cleanError(this.key); this.resource.markOutdated(this.key); - }, + }), message, options, ); diff --git a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts index cb3bc2225f..250a93db2b 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts @@ -50,16 +50,31 @@ export class ResourceMetadata { return this.metadata.has(this.getMetadataKeyRef(key)); } - every(param: ResourceKey, predicate: (metadata: TMetadata) => boolean): boolean { + every(predicate: (metadata: TMetadata) => boolean): boolean; + every(param: ResourceKey, predicate: (metadata: TMetadata) => boolean): boolean; + every(param: ResourceKey | ((metadata: TMetadata) => boolean), predicate?: (metadata: TMetadata) => boolean): boolean { + if (!predicate) { + predicate = param as (metadata: TMetadata) => boolean; + for (const metadata of this.values()) { + if (!predicate(metadata)) { + return false; + } + } + return true; + } + + param = param as ResourceKey; + predicate = predicate as MetadataCallback; + if (!this.has(param)) { return false; } - return !this.some(param, key => !predicate(key)); + return !this.some(param, key => !predicate!(key)); } + some(predicate: MetadataCallback): boolean; some(param: ResourceKey, predicate: MetadataCallback): boolean; - some(param: ResourceKey | MetadataCallback, predicate?: MetadataCallback): boolean { if (!predicate) { predicate = param as (metadata: TMetadata) => boolean; diff --git a/webapp/packages/core-resource/src/index.ts b/webapp/packages/core-resource/src/index.ts index cff8c3ac8e..9e3b099a41 100644 --- a/webapp/packages/core-resource/src/index.ts +++ b/webapp/packages/core-resource/src/index.ts @@ -12,6 +12,8 @@ export { } from './Resource/CachedResourceOffsetPageKeys'; export * from './Resource/CachedTreeResource/CachedTreeResource'; export * from './Resource/ICachedResourceMetadata'; +export * from './Resource/IResource'; +export * from './Resource/Resource'; export * from './Resource/ResourceAlias'; export * from './Resource/ResourceError'; export * from './Resource/ResourceKey'; diff --git a/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts b/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts index e5d72aca6e..5052c14914 100644 --- a/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts +++ b/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts @@ -8,7 +8,7 @@ import { Connectable, connectable, filter, map, merge, Observable, Subject } from 'rxjs'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import type { CachedResource } from '@cloudbeaver/core-resource'; +import type { IResource } from '@cloudbeaver/core-resource'; import { compose } from '@cloudbeaver/core-utils'; import type { IBaseServerEvent, IServerEventCallback, IServerEventEmitter, Subscription } from './IServerEventEmitter'; @@ -29,8 +29,8 @@ export abstract class TopicEventHandler< readonly eventsSubject: Connectable; private subscription: Subscription | null; - private readonly activeResources: Array>; - private readonly subscribedResources: Map, ISubscribedResourceInfo>; + private readonly activeResources: Array>; + private readonly subscribedResources: Map, ISubscribedResourceInfo>; private readonly serverSubject?: Observable; private readonly subject: Subject; constructor(private readonly topic: string, private readonly emitter: IServerEventEmitter) { @@ -56,7 +56,7 @@ export abstract class TopicEventHandler< id: TEventID, callback: IServerEventCallback, mapTo: (event: TEvent) => T = event => event as unknown as T, - resource?: CachedResource, + resource?: IResource, ): Subscription { if (resource) { this.registerResource(resource); @@ -82,7 +82,7 @@ export abstract class TopicEventHandler< callback: IServerEventCallback, mapTo: (param: TEvent) => T = event => event as unknown as T, filterFn: (param: TEvent) => boolean = () => true, - resource?: CachedResource, + resource?: IResource, ): Subscription { if (resource) { this.registerResource(resource); @@ -104,7 +104,7 @@ export abstract class TopicEventHandler< return this; } - private resourceUseHandler(resource: CachedResource) { + private resourceUseHandler(resource: IResource) { const index = this.activeResources.indexOf(resource); if (index !== -1) { @@ -124,7 +124,7 @@ export abstract class TopicEventHandler< } } - private removeActiveResource(resource: CachedResource) { + private removeActiveResource(resource: IResource) { this.activeResources.splice(this.activeResources.indexOf(resource), 1); if (this.activeResources.length === 0) { @@ -134,7 +134,7 @@ export abstract class TopicEventHandler< } } - private registerResource(resource: CachedResource): void { + private registerResource(resource: IResource): void { let info = this.subscribedResources.get(resource); if (!info) { @@ -150,7 +150,7 @@ export abstract class TopicEventHandler< info.listeners++; } - private removeResource(resource: CachedResource): void { + private removeResource(resource: IResource): void { const info = this.subscribedResources.get(resource); if (info) { diff --git a/webapp/packages/core-view/src/Menu/MenuService.ts b/webapp/packages/core-view/src/Menu/MenuService.ts index 3c877ac2e4..25671cdb6b 100644 --- a/webapp/packages/core-view/src/Menu/MenuService.ts +++ b/webapp/packages/core-view/src/Menu/MenuService.ts @@ -123,17 +123,17 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC const local = contexts.get(DATA_CONTEXT_MENU_LOCAL); return (creator: IMenuItemsCreator) => { - if (local) { - if (!creator.menus && !creator.contexts) { + if (creator.menus) { + const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false)); + + if (!applicable) { return false; } + } - if (creator.menus) { - const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false)); - - if (!applicable) { - return false; - } + if (local) { + if (!creator.menus && !creator.contexts) { + return false; } if (creator.contexts) { @@ -151,14 +151,6 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC return false; } - if (creator.menus) { - const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false)); - - if (!applicable) { - return false; - } - } - return true; }; } diff --git a/webapp/packages/plugin-administration/src/Administration/Administration.tsx b/webapp/packages/plugin-administration/src/Administration/Administration.tsx index 5d73b38c6e..bb8c6276db 100644 --- a/webapp/packages/plugin-administration/src/Administration/Administration.tsx +++ b/webapp/packages/plugin-administration/src/Administration/Administration.tsx @@ -147,11 +147,9 @@ export const Administration = observer>(function - - - - - + + + optionsPanelService.close()} /> diff --git a/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx b/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx index 6977c1f519..06ce0f9b84 100644 --- a/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx +++ b/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx @@ -39,11 +39,19 @@ export const ItemContent = observer(function ItemContent({ activeScreen, if (sub) { const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent(); - return ; + return ( + + + + ); } } const Component = item.getContentComponent(); - return ; + return ( + + + + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx index d4e3f7ba97..d4908a73e9 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx @@ -13,6 +13,7 @@ import { Combobox, Group, GroupTitle, ITag, s, Tag, Tags, useResource, useS, use import { DBDriverResource } from '@cloudbeaver/core-connections'; import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource'; import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { isDefined } from '@cloudbeaver/core-utils'; import style from './ServerConfigurationDriversForm.m.css'; @@ -25,7 +26,7 @@ export const ServerConfigurationDriversForm = observer(function ServerCon const translate = useTranslate(); const driversResource = useResource(ServerConfigurationDriversForm, DBDriverResource, CachedMapAllKey); - const drivers = driversResource.resource.values.slice().sort(driversResource.resource.compare); + const drivers = driversResource.data.filter(isDefined).sort(driversResource.resource.compare); const tags: ITag[] = driversResource.resource .get(resourceKeyList(serverConfig.disabledDrivers || [])) diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index efe0d11eb7..e604f04af4 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -3,8 +3,8 @@ export default [ ['administration_server_configuration_save_confirmation_message', 'You are about to change critical settings. Are you sure?'], ['administration_configuration_wizard_welcome', 'Welcome'], - ['administration_configuration_wizard_welcome_step_description', 'Welcome to CloudBeaver'], - ['administration_configuration_wizard_welcome_title', 'Welcome to CloudBeaver, cloud database management system!'], + ['administration_configuration_wizard_welcome_step_description', 'Welcome to {alias:product_full_name}'], + ['administration_configuration_wizard_welcome_title', 'Welcome to {alias:product_full_name}, cloud database management system!'], [ 'administration_configuration_wizard_welcome_message', 'The easy configuration wizard will guide you through several simple steps to set up the CloudBeaver server. You will need to set server information and administrator credentials. You can set up additional server parameters once the easy configuration is completed.', diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx index 867e158c80..92e7e78aeb 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx @@ -11,6 +11,7 @@ import { ColoredContainer, Container, FieldCheckbox, Group, GroupTitle, Placehol import { useService } from '@cloudbeaver/core-di'; import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; +import { AdministrationUsersManagementService } from '../../../../AdministrationUsersManagementService'; import type { UserFormProps } from '../AdministrationUserFormService'; import { UserFormInfoCredentials } from './UserFormInfoCredentials'; import { UserFormInfoMetaParameters } from './UserFormInfoMetaParameters'; @@ -23,10 +24,12 @@ export const UserFormInfo: TabContainerPanelComponent = observer( const tab = useTab(tabId); const tabState = useTabState(); const userFormInfoPartService = useService(UserFormInfoPartService); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); - useAutoLoad(UserFormInfo, tabState, tab.selected); + useAutoLoad(UserFormInfo, [tabState, ...administrationUsersManagementService.loaders], tab.selected); const disabled = tabState.isLoading(); + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; // let info: TLocalizationToken | null = null; // if (formState.mode === FormMode.Edit && tabState.isChanged()) { @@ -43,7 +46,7 @@ export const UserFormInfo: TabContainerPanelComponent = observer( {translate('authentication_user_status')} - + {translate('authentication_user_enabled')} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts index 2a8edb1a49..0122c1cea0 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts @@ -11,6 +11,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; import { ACTION_CREATE, ActionService, MenuService } from '@cloudbeaver/core-view'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService'; import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION'; import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from '../ADMINISTRATION_ITEM_USER_CREATE_PARAM'; import { CreateUserService } from './CreateUserService'; @@ -22,6 +23,7 @@ export class CreateUserBootstrap extends Bootstrap { private readonly createUserService: CreateUserService, private readonly menuService: MenuService, private readonly actionService: ActionService, + private readonly administrationUsersManagementService: AdministrationUsersManagementService, ) { super(); } @@ -37,7 +39,7 @@ export class CreateUserBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'users-table-base', isActionApplicable: (context, action) => { - if (action === ACTION_CREATE) { + if (action === ACTION_CREATE && !this.administrationUsersManagementService.externalUserProviderEnabled) { return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); } @@ -52,9 +54,10 @@ export class CreateUserBootstrap extends Bootstrap { return false; }, - getLoader: (context, action) => { - return getCachedMapResourceLoaderState(this.authProvidersResource, () => CachedMapAllKey); - }, + getLoader: () => [ + getCachedMapResourceLoaderState(this.authProvidersResource, () => CachedMapAllKey), + ...this.administrationUsersManagementService.loaders, + ], handler: (context, action) => { switch (action) { case ACTION_CREATE: diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx index 8fdb3d96f4..7832f8b508 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx @@ -9,10 +9,21 @@ import { observer } from 'mobx-react-lite'; import styled, { css, use } from 'reshadow'; import { AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; -import { Checkbox, Loader, Placeholder, TableColumnValue, TableItem, TableItemExpand, TableItemSelect, useTranslate } from '@cloudbeaver/core-blocks'; +import { + Checkbox, + Loader, + Placeholder, + TableColumnValue, + TableItem, + TableItemExpand, + TableItemSelect, + useAutoLoad, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService'; import { UsersAdministrationService } from '../UsersAdministrationService'; import { UserEdit } from './UserEdit'; @@ -36,8 +47,11 @@ export const User = observer(function User({ user, displayAuthRole, selec const teams = user.grantedTeams.join(', '); const usersService = useService(UsersResource); const notificationService = useService(NotificationService); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); const translate = useTranslate(); + useAutoLoad(User, administrationUsersManagementService.loaders); + async function handleEnabledCheckboxChange(enabled: boolean) { try { await usersService.enableUser(user.userId, enabled); @@ -50,6 +64,8 @@ export const User = observer(function User({ user, displayAuthRole, selec ? translate('administration_teams_team_granted_users_permission_denied') : undefined; + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; + return styled(styles)( {selectable && ( @@ -74,7 +90,7 @@ export const User = observer(function User({ user, displayAuthRole, selec diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx index e778893bf2..a42168a3ad 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks'; import { MenuBar, MenuBarItemStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx index 1ce6337740..29bdd6d386 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx @@ -10,9 +10,10 @@ import styled from 'reshadow'; import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; import { AuthRolesResource } from '@cloudbeaver/core-authentication'; -import { ColoredContainer, Container, Group, Placeholder, useResource, useStyles } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Container, Group, Placeholder, useAutoLoad, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService'; import { CreateUser } from './CreateUser'; import { CreateUserService } from './CreateUserService'; import { UsersTableFilters } from './Filters/UsersTableFilters'; @@ -30,13 +31,16 @@ export const UsersPage = observer(function UsersPage({ sub, param }) { const style = useStyles(ADMINISTRATION_TOOLS_PANEL_STYLES); const createUserService = useService(CreateUserService); const authRolesResource = useResource(UsersPage, AuthRolesResource, undefined); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); + useAutoLoad(UsersPage, administrationUsersManagementService.loaders); const filters = useUsersTableFilters(); const table = useUsersTable(filters); const create = param === 'create'; const displayAuthRole = authRolesResource.data.length > 0; const loading = authRolesResource.isLoading() || table.loadableState.isLoading(); + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; return styled(style)( @@ -45,7 +49,7 @@ export const UsersPage = observer(function UsersPage({ sub, param }) { - {create && createUserService.state && ( + {create && createUserService.state && !userManagementDisabled && ( diff --git a/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts b/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts new file mode 100644 index 0000000000..3c97e3b681 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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, makeObservable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; + +import { externalUserProviderStatusContext } from './externalUserProviderStatusContext'; + +@injectable() +export class AdministrationUsersManagementService { + get externalUserProviderEnabled(): boolean { + const contexts = this.getExternalUserProviderStatus.execute(); + const projectsContext = contexts.getContext(externalUserProviderStatusContext); + + return projectsContext.externalUserProviderEnabled; + } + + get loaders(): ILoadableState[] { + const contexts = this.getExternalUserProviderStatus.execute(); + const projectsContext = contexts.getContext(externalUserProviderStatusContext); + + return projectsContext.loaders; + } + + readonly getExternalUserProviderStatus: ISyncExecutor; + + constructor() { + makeObservable(this, { + externalUserProviderEnabled: computed, + loaders: computed({ equals: (a, b) => isArraysEqual(a, b) }), + }); + + this.getExternalUserProviderStatus = new SyncExecutor(); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts b/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts new file mode 100644 index 0000000000..9758e7223d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { ILoadableState } from '@cloudbeaver/core-utils'; + +interface IExternalUserProviderStatusContext { + externalUserProviderEnabled: boolean; + loaders: ILoadableState[]; + setExternalUserProviderStatus(enabled: boolean): void; + addLoader(loader: ILoadableState): void; +} + +export function externalUserProviderStatusContext(): IExternalUserProviderStatusContext { + return { + externalUserProviderEnabled: false, + loaders: [], + setExternalUserProviderStatus(enabled: boolean) { + this.externalUserProviderEnabled = enabled; + }, + addLoader(loader: ILoadableState) { + this.loaders.push(loader); + }, + }; +} diff --git a/webapp/packages/plugin-authentication-administration/src/index.ts b/webapp/packages/plugin-authentication-administration/src/index.ts index 84d37c2ac7..5d86833a85 100644 --- a/webapp/packages/plugin-authentication-administration/src/index.ts +++ b/webapp/packages/plugin-authentication-administration/src/index.ts @@ -17,3 +17,5 @@ export * from './Administration/Users/UserForm/Info/UserFormInfoPartService'; export * from './Administration/IdentityProviders/IAuthConfigurationFormProps'; export * from './Administration/IdentityProviders/AuthConfigurationFormService'; export * from './Menus/MENU_USERS_ADMINISTRATION'; +export * from './AdministrationUsersManagementService'; +export * from './externalUserProviderStatusContext'; diff --git a/webapp/packages/plugin-authentication-administration/src/manifest.ts b/webapp/packages/plugin-authentication-administration/src/manifest.ts index 1bd7799b4c..3a47539343 100644 --- a/webapp/packages/plugin-authentication-administration/src/manifest.ts +++ b/webapp/packages/plugin-authentication-administration/src/manifest.ts @@ -30,6 +30,7 @@ import { UsersAdministrationNavigationService } from './Administration/Users/Use import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService'; import { CreateUserBootstrap } from './Administration/Users/UsersTable/CreateUserBootstrap'; import { CreateUserService } from './Administration/Users/UsersTable/CreateUserService'; +import { AdministrationUsersManagementService } from './AdministrationUsersManagementService'; import { AuthenticationLocaleService } from './AuthenticationLocaleService'; import { PluginBootstrap } from './PluginBootstrap'; @@ -64,5 +65,6 @@ export const manifest: PluginManifest = { UserFormOriginPartBootstrap, UserFormConnectionAccessPartBootstrap, UserFormInfoPartService, + AdministrationUsersManagementService, ], }; diff --git a/webapp/packages/plugin-devtools/src/DevToolsService.ts b/webapp/packages/plugin-devtools/src/DevToolsService.ts index 9d0f33e5cf..3e291d82e7 100644 --- a/webapp/packages/plugin-devtools/src/DevToolsService.ts +++ b/webapp/packages/plugin-devtools/src/DevToolsService.ts @@ -14,6 +14,7 @@ import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; interface IDevToolsSettings { enabled: boolean; distributed: boolean; + configuration: boolean; } const DEVTOOLS = 'devtools'; @@ -28,6 +29,10 @@ export class DevToolsService { return this.settings.distributed; } + get isConfiguration(): boolean { + return this.settings.configuration; + } + private readonly settings: IDevToolsSettings; constructor(private readonly serverConfigResource: ServerConfigResource, private readonly autoSaveService: LocalStorageSaveService) { @@ -37,23 +42,29 @@ export class DevToolsService { settings: observable, }); this.autoSaveService.withAutoSave(DEVTOOLS, this.settings, getDefaultDevToolsSettings); - this.serverConfigResource.onDataUpdate.addHandler(this.syncDistributedMode.bind(this)); + this.serverConfigResource.onDataUpdate.addHandler(this.syncSettingsOverride.bind(this)); } switch() { this.settings.enabled = !this.settings.enabled; - this.syncDistributedMode(); + this.syncSettingsOverride(); } setDistributedMode(distributed: boolean) { this.settings.distributed = distributed; - this.syncDistributedMode(); + this.syncSettingsOverride(); + } + + setConfigurationMode(configuration: boolean) { + this.settings.configuration = configuration; + this.syncSettingsOverride(); } - private syncDistributedMode() { + private syncSettingsOverride() { if (this.isEnabled) { if (this.serverConfigResource.data) { this.serverConfigResource.data.distributed = this.isDistributed; + this.serverConfigResource.data.configurationMode = this.isConfiguration; } } } @@ -61,7 +72,8 @@ export class DevToolsService { function getDefaultDevToolsSettings(): IDevToolsSettings { return { - enabled: false, + enabled: process.env.NODE_ENV === 'development', distributed: false, + configuration: false, }; } diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index ac43aac65e..046818ea5d 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -14,6 +14,7 @@ import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile'; import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS'; +import { ACTION_DEVTOOLS_MODE_CONFIGURATION } from './actions/ACTION_DEVTOOLS_MODE_CONFIGURATION'; import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED'; import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH'; import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem'; @@ -97,7 +98,16 @@ export class PluginBootstrap extends Bootstrap { ]; } - return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, MENU_PLUGINS, ...items]; + return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, ACTION_DEVTOOLS_MODE_CONFIGURATION, MENU_PLUGINS, ...items]; + }, + }); + + this.actionService.addHandler({ + id: 'devtools-mode-configuration', + isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_MODE_CONFIGURATION, + isChecked: () => this.devToolsService.isConfiguration, + handler: () => { + this.devToolsService.setConfigurationMode(!this.devToolsService.isConfiguration); }, }); diff --git a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts new file mode 100644 index 0000000000..74b3bdf1ef --- /dev/null +++ b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DEVTOOLS_MODE_CONFIGURATION = createAction('devtools-mode-configuration', { + type: 'checkbox', + label: 'Easy config mode', + tooltip: 'Enable easy config mode', +}); From 3a92d7d23d8dfbe5ce8d895caa695cbfc0b9cd16 Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Thu, 19 Oct 2023 12:26:55 +0200 Subject: [PATCH 10/18] CB-3930 use rm provider for rm fs (#2038) --- .../src/io/cloudbeaver/BaseWebProjectImpl.java | 6 ++++++ .../service/rm/fs/RMVirtualFileSystem.java | 7 ++++--- .../rm/fs/RMVirtualFileSystemProvider.java | 16 +++++----------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java index c59670bf84..f7d5c46e2d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java @@ -62,6 +62,12 @@ public RMController getResourceController() { return resourceController; } + @NotNull + @Override + public RMProject getRMProject() { + return project; + } + @Override public boolean isVirtual() { return true; diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java index 122362c65f..871179c58c 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java @@ -16,13 +16,13 @@ */ package io.cloudbeaver.service.rm.fs; -import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.rm.nio.RMNIOFileSystemProvider; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystemRoot; +import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -35,8 +35,9 @@ public class RMVirtualFileSystem implements DBFVirtualFileSystem { @NotNull private final RMProject rmProject; - public RMVirtualFileSystem(@NotNull WebSession webSession, @NotNull RMProject rmProject) { - this.rmNioFileSystemProvider = new RMNIOFileSystemProvider(webSession.getRmController()); + + public RMVirtualFileSystem(@NotNull RMController rmController, @NotNull RMProject rmProject) { + this.rmNioFileSystemProvider = new RMNIOFileSystemProvider(rmController); this.rmProject = rmProject; } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java index e5355ee310..33617f913a 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java @@ -16,13 +16,12 @@ */ package io.cloudbeaver.service.rm.fs; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.fs.DBFFileSystemProvider; import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; +import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; public class RMVirtualFileSystemProvider implements DBFFileSystemProvider { @@ -33,16 +32,11 @@ public DBFVirtualFileSystem[] getAvailableFileSystems( @NotNull DBRProgressMonitor monitor, @NotNull DBPProject project ) { - var session = project.getSessionContext().getPrimaryAuthSpace(); - if (!(session instanceof WebSession)) { + if (!(project instanceof RMControllerProvider)) { return new DBFVirtualFileSystem[0]; } - WebSession webSession = (WebSession) session; - WebProjectImpl webProject = webSession.getProjectById(project.getId()); - if (webProject == null) { - log.warn(String.format("Project %s not found in session %s", project.getId(), webSession.getSessionId())); - return new DBFVirtualFileSystem[0]; - } - return new DBFVirtualFileSystem[]{new RMVirtualFileSystem(webSession, webProject.getRmProject())}; + RMControllerProvider rmControllerProvider = (RMControllerProvider) project; + return new DBFVirtualFileSystem[]{new RMVirtualFileSystem(rmControllerProvider.getResourceController(), + rmControllerProvider.getRMProject())}; } } From be59a4dab97984e6253ae5004553e5bdc815feed Mon Sep 17 00:00:00 2001 From: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:29:31 +0400 Subject: [PATCH 11/18] Hotfix 4144 validate blob (#2072) * CB-4144. Fix regex --- .../io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java index f8ef14585b..70e26a8c90 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -53,7 +53,7 @@ public class WebSQLFileLoaderServlet extends WebServiceServletBase { private static final String FILE_ID = "fileId"; - private static final String FORBIDDEN_CHARACTERS_FILE_REGEX = "(?U)[\\w.$()@ -]+"; + private static final String FORBIDDEN_CHARACTERS_FILE_REGEX = "(?U)[$()@ /]+"; private static final Gson gson = new GsonBuilder() .serializeNulls() @@ -90,7 +90,7 @@ protected void processServiceRequest( String fileId = JSONUtils.getString(variables, FILE_ID); - if (fileId != null && !fileId.matches(FORBIDDEN_CHARACTERS_FILE_REGEX)) { + if (fileId != null && !fileId.matches(FORBIDDEN_CHARACTERS_FILE_REGEX) && !fileId.startsWith(".")) { Path file = tempFolder.resolve(fileId); try { Files.write(file, request.getPart("fileData").getInputStream().readAllBytes()); From 4645a8e3d1dbf7e1d4a19c9d08bc667ad771aee5 Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Fri, 20 Oct 2023 18:14:24 +0200 Subject: [PATCH 12/18] Cb 3959 UI file system navigator view (#2069) * CB-4061 file systems feature * CB-4061 return rm fs navigator nodes * CB-3959 fix tests * CB-3959 encode spaces in rm uri --- .../io.cloudbeaver.service.fs/plugin.xml | 4 ++- .../rm/nio/RMNIOFileSystemProvider.java | 3 ++ .../io/cloudbeaver/service/rm/nio/RMPath.java | 36 +++++++++++++------ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/server/bundles/io.cloudbeaver.service.fs/plugin.xml b/server/bundles/io.cloudbeaver.service.fs/plugin.xml index 45540f23a6..09ad0fdeca 100644 --- a/server/bundles/io.cloudbeaver.service.fs/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.fs/plugin.xml @@ -8,5 +8,7 @@ class="io.cloudbeaver.service.fs.WebServiceBindingFS"> - + + + diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java index 676b28dddc..d1c367a781 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.io.OutputStream; import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; @@ -80,6 +82,7 @@ public Path getPath(URI uri) { } RMNIOFileSystem rmNioFileSystem = new RMNIOFileSystem(projectId, this); String resourcePath = uri.getPath(); + resourcePath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8); if (CommonUtils.isNotEmpty(resourcePath) && projectId == null) { throw new IllegalArgumentException("Project is not specified in URI"); } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java index c38642269e..8b5d0eb37d 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java @@ -25,9 +25,14 @@ import java.io.IOException; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; public class RMPath extends NIOPath { @NotNull @@ -71,7 +76,7 @@ public Path getFileName() { if (ArrayUtils.isEmpty(parts)) { return this; } - return new RMPath(rmNioFileSystem, parts[parts.length - 1]); + return new RMPath(new RMNIOFileSystem(null, getFileSystem().rmProvider()), parts[parts.length - 1]); } @Override @@ -122,22 +127,31 @@ public Path resolve(String other) { @Override public URI toUri() { var fileSystem = getFileSystem(); - var uriBuilder = new StringBuilder(fileSystem.provider().getScheme()) - .append("://"); - - if (rmProjectId != null) { - uriBuilder.append(rmProjectId); + var uriBuilder = new StringBuilder(); + if (isAbsolute()) { + uriBuilder.append(fileSystem.provider().getScheme()) + .append("://"); } - String rmResourcePath = getResourcePath(); - if (rmResourcePath != null) { - uriBuilder.append(fileSystem.getSeparator()) - .append(rmResourcePath); - } + var paths = new ArrayList(); + paths.add(rmProjectId); + paths.add(getResourcePath()); + + uriBuilder.append( + paths.stream() + .filter(Objects::nonNull) + .map(s -> URLEncoder.encode(s, StandardCharsets.UTF_8)) + .collect(Collectors.joining(fileSystem.getSeparator())) + ); return URI.create(uriBuilder.toString()); } + @Override + public boolean isAbsolute() { + return rmNioFileSystem.getRmProjectId() != null; + } + @Override public Path toAbsolutePath() { if (isAbsolute()) { From 518410c2e8f6991a6e03abcd39681297c30da263 Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 20 Oct 2023 19:51:04 +0300 Subject: [PATCH 13/18] Fix/cb-4141/localization (#2071) (#2075) --- .../product-default/src/LocaleService.ts | 35 +++++++++++++++++++ .../product-default/src/locales/en.ts | 4 +++ .../product-default/src/locales/it.ts | 4 +++ .../product-default/src/locales/ru.ts | 4 +++ .../product-default/src/locales/zh.ts | 4 +++ .../packages/product-default/src/manifest.ts | 3 +- 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 webapp/packages/product-default/src/LocaleService.ts create mode 100644 webapp/packages/product-default/src/locales/en.ts create mode 100644 webapp/packages/product-default/src/locales/it.ts create mode 100644 webapp/packages/product-default/src/locales/ru.ts create mode 100644 webapp/packages/product-default/src/locales/zh.ts diff --git a/webapp/packages/product-default/src/LocaleService.ts b/webapp/packages/product-default/src/LocaleService.ts new file mode 100644 index 0000000000..e3649a06b4 --- /dev/null +++ b/webapp/packages/product-default/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise {} + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + case 'it': + return (await import('./locales/it')).default; + case 'zh': + return (await import('./locales/zh')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/product-default/src/locales/en.ts b/webapp/packages/product-default/src/locales/en.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/en.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/locales/it.ts b/webapp/packages/product-default/src/locales/it.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/it.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/locales/ru.ts b/webapp/packages/product-default/src/locales/ru.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/ru.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/locales/zh.ts b/webapp/packages/product-default/src/locales/zh.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/zh.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/manifest.ts b/webapp/packages/product-default/src/manifest.ts index 6ad91cadbd..7663e77c49 100644 --- a/webapp/packages/product-default/src/manifest.ts +++ b/webapp/packages/product-default/src/manifest.ts @@ -7,6 +7,7 @@ */ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService'; import { ProductBootstrap } from './ProductBootstrap'; import { ProductConfigService } from './ProductConfigService'; @@ -15,5 +16,5 @@ export const defaultProductManifest: PluginManifest = { name: 'Default Product', }, - providers: [ProductBootstrap, ProductConfigService], + providers: [ProductBootstrap, ProductConfigService, LocaleService], }; From 81ce6b6ac70c82015e95bf05d9d91169fbc71aed Mon Sep 17 00:00:00 2001 From: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:32:35 +0300 Subject: [PATCH 14/18] CB_2323 chore: update README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b82b3ae99c..7943c2202d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### CloudBeaver 23.2.3 - 2023-10-23 + +- The SSL option is available for establishing a connection in SQL Server; +- Added the ability to edit binary values in a table; +- Different bug fixes and enhancements have been made. + ### CloudBeaver 23.2.2 - 2023-10-09 - The 'Save credentials' checkbox has been removed from a template creating form as credentials are not stored in templates; From 997936328ed8699a7cfc05969d8d05e16681c763 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 23 Oct 2023 22:53:24 +0300 Subject: [PATCH 15/18] fix(core-resource): error getter (#2077) --- webapp/packages/core-resource/src/Resource/Resource.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/packages/core-resource/src/Resource/Resource.ts b/webapp/packages/core-resource/src/Resource/Resource.ts index 12cc9a18f6..40357c32a5 100644 --- a/webapp/packages/core-resource/src/Resource/Resource.ts +++ b/webapp/packages/core-resource/src/Resource/Resource.ts @@ -122,6 +122,10 @@ export abstract class Resource< getException(param: ResourceKeyList): Error[] | null; getException(param: ResourceKey): Error[] | Error | null; getException(param: ResourceKey): Error[] | Error | null { + if (param === undefined) { + param = CachedResourceParamKey; + } + if (isResourceKeyList(param)) { return this.metadata.map(param, metadata => metadata?.exception || null).filter((exception): exception is Error => exception !== null); } From 98197c8829f216c641bb5700fcffd214e3193a6b Mon Sep 17 00:00:00 2001 From: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:46:55 +0400 Subject: [PATCH 16/18] CB-4099. Create logic for save comfig and user for CB TE. (#2067) * CB-4099. Create logic for save comfig and user for CB TE. * CB-4099. Fixed checkstyle * CB-4099. Refactor after review --------- Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../src/io/cloudbeaver/server/CBApplication.java | 6 +++--- .../src/io/cloudbeaver/server/CBApplicationCE.java | 5 ++--- .../cloudbeaver/service/admin/impl/WebServiceAdmin.java | 9 ++++++++- .../service/security/CBEmbeddedSecurityController.java | 3 +-- .../io/cloudbeaver/service/security/db/CBDatabase.java | 9 ++++----- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 946ba17e73..90b1b50d0f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.server; +import org.jkiss.dbeaver.model.auth.AuthInfo; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; @@ -24,7 +25,6 @@ import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; -import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.registry.WebDriverRegistry; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.jetty.CBJettyServer; @@ -825,7 +825,7 @@ public synchronized void finishConfiguration( @NotNull String newServerURL, @NotNull String adminName, @Nullable String adminPassword, - @NotNull List authInfoList, + @NotNull List authInfoList, long sessionExpireTime, @NotNull CBAppConfig appConfig, @Nullable SMCredentialsProvider credentialsProvider @@ -886,7 +886,7 @@ protected Map readRuntimeConfigurationProperties() throws DBExce protected abstract void finishSecurityServiceConfiguration( @NotNull String adminName, @Nullable String adminPassword, - @NotNull List authInfoList + @NotNull List authInfoList ) throws DBException; public synchronized void flushConfiguration(SMCredentialsProvider credentialsProvider) throws DBException { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java index 456970f70b..cfb125e488 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java @@ -16,9 +16,9 @@ */ package io.cloudbeaver.server; +import org.jkiss.dbeaver.model.auth.AuthInfo; import io.cloudbeaver.auth.NoAuthCredentialsProvider; import io.cloudbeaver.model.rm.local.LocalResourceController; -import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.service.security.CBEmbeddedSecurityController; import io.cloudbeaver.service.security.EmbeddedSecurityControllerFactory; import org.jkiss.code.NotNull; @@ -31,7 +31,6 @@ import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; -import org.jkiss.dbeaver.registry.BasePlatformImpl; import org.jkiss.dbeaver.registry.LocalFileController; import org.jkiss.dbeaver.runtime.DBWorkbench; @@ -94,7 +93,7 @@ protected void shutdown() { protected void finishSecurityServiceConfiguration( @NotNull String adminName, @Nullable String adminPassword, - @NotNull List authInfoList + @NotNull List authInfoList ) throws DBException { if (securityController instanceof CBEmbeddedSecurityController) { ((CBEmbeddedSecurityController) securityController).finishConfiguration(adminName, adminPassword, authInfoList); diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 9a903f9364..47df0055d1 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -40,6 +40,7 @@ import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.auth.AuthInfo; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.security.*; @@ -514,6 +515,7 @@ public boolean configureServer(WebSession webSession, Map params adminName = curUser == null ? null : curUser.getUserId(); adminPassword = null; } + List authInfos = new ArrayList<>(); List authInfoList = webSession.getAllAuthInfo(); if (CommonUtils.isEmpty(adminName)) { // Try to get admin name from existing authentications (first one) @@ -524,6 +526,11 @@ public boolean configureServer(WebSession webSession, Map params if (CommonUtils.isEmpty(adminName)) { adminName = CBConstants.DEFAULT_ADMIN_NAME; } + for (WebAuthInfo webAuthInfo : authInfoList) { + authInfos.add(new AuthInfo( + webAuthInfo.getAuthProviderDescriptor().getId(), + webAuthInfo.getUserCredentials())); + } // Patch configuration by services for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance().getWebServices(DBWServiceServerConfigurator.class)) { @@ -541,7 +548,7 @@ public boolean configureServer(WebSession webSession, Map params serverURL, adminName, adminPassword, - authInfoList, + authInfos, sessionExpireTime, appConfig, webSession diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 0a06993d3d..8c9c332dd1 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -26,7 +26,6 @@ import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; -import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.registry.WebMetaParametersRegistry; @@ -2625,7 +2624,7 @@ public void shutdown() { public void finishConfiguration( @NotNull String adminName, @Nullable String adminPassword, - @NotNull List authInfoList + @NotNull List authInfoList ) throws DBException { database.finishConfiguration(adminName, adminPassword, authInfoList); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java index 83607ab788..13475c8960 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java @@ -20,7 +20,6 @@ import com.google.gson.GsonBuilder; import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; import io.cloudbeaver.model.app.WebApplication; -import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.utils.WebAppUtils; @@ -32,6 +31,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBConstants; +import org.jkiss.dbeaver.model.auth.AuthInfo; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCTransaction; @@ -246,7 +246,7 @@ protected PoolingDataSource initConnectionPool( public void finishConfiguration( @NotNull String adminName, @Nullable String adminPassword, - @NotNull List authInfoList + @NotNull List authInfoList ) throws DBException { if (!application.isConfigurationMode()) { throw new DBException("Database is already configured"); @@ -264,12 +264,11 @@ public void finishConfiguration( createAdminUser(adminName, adminPassword); // Associate all auth credentials with admin user - for (WebAuthInfo ai : authInfoList) { + for (AuthInfo ai : authInfoList) { if (!ai.getAuthProvider().equals(LocalAuthProviderConstants.PROVIDER_ID)) { - WebAuthProviderDescriptor authProvider = ai.getAuthProviderDescriptor(); Map userCredentials = ai.getUserCredentials(); if (!CommonUtils.isEmpty(userCredentials)) { - adminSecurityController.setUserCredentials(adminName, authProvider.getId(), userCredentials); + adminSecurityController.setUserCredentials(adminName, ai.getAuthProvider(), userCredentials); } } } From f67835d2d2de847be7450e72a9d1700a7584bff8 Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:49:51 +0200 Subject: [PATCH 17/18] CB-2700 delimeter support (#2068) Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../service/sql/WebSQLProcessor.java | 114 +++++++++--------- .../service/sql/WebSQLQueryDataContainer.java | 5 + 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index 3ee8dffdd3..4b9dfc3b7e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -43,9 +43,7 @@ import org.jkiss.dbeaver.model.navigator.DBNDatabaseItem; import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.sql.SQLQuery; -import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; -import org.jkiss.dbeaver.model.sql.SQLUtils; +import org.jkiss.dbeaver.model.sql.*; import org.jkiss.dbeaver.model.sql.parser.SQLParserContext; import org.jkiss.dbeaver.model.sql.parser.SQLRuleManager; import org.jkiss.dbeaver.model.sql.parser.SQLScriptParser; @@ -173,7 +171,7 @@ public WebSQLExecuteInfo processQuery( long startTime = System.currentTimeMillis(); WebSQLExecuteInfo executeInfo = new WebSQLExecuteInfo(); - DBSDataContainer dataContainer = new WebSQLQueryDataContainer(connection.getDataSource(), sql); + var dataContainer = new WebSQLQueryDataContainer(connection.getDataSource(), sql); DBCExecutionContext context = getExecutionContext(dataContainer); @@ -198,63 +196,69 @@ public WebSQLExecuteInfo processQuery( ruleManager, document); - SQLQuery sqlQuery = (SQLQuery) SQLScriptParser.extractActiveQuery(parserContext, 0, sql.length()); - - DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { - try (DBCSession session = context.openSession(monitor, resolveQueryPurpose(dataFilter), "Execute SQL")) { - AbstractExecutionSource source = new AbstractExecutionSource( - dataContainer, - session.getExecutionContext(), - WebSQLProcessor.this, - sqlQuery); - - try (DBCStatement dbStat = DBUtils.makeStatement( - source, - session, - DBCStatementType.SCRIPT, - sqlQuery, - webDataFilter.getOffset(), - webDataFilter.getLimit())) - { - SqlOutputLogReaderJob sqlOutputLogReaderJob = null; - if (readLogs) { - DBPDataSource dataSource = context.getDataSource(); - DBCServerOutputReader dbcServerOutputReader = DBUtils.getAdapter(DBCServerOutputReader.class, dataSource); - if (dbcServerOutputReader == null) { - dbcServerOutputReader = new DefaultServerOutputReader(); + SQLScriptElement element = SQLScriptParser.extractActiveQuery(parserContext, 0, sql.length()); + + if (element instanceof SQLControlCommand command) { + dataContainer.getScriptContext().executeControlCommand(command); + WebSQLQueryResults stats = new WebSQLQueryResults(webSession, dataFormat); + executeInfo.setResults(new WebSQLQueryResults[]{stats}); + } else if (element instanceof SQLQuery sqlQuery) { + DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { + try (DBCSession session = context.openSession(monitor, resolveQueryPurpose(dataFilter), "Execute SQL")) { + AbstractExecutionSource source = new AbstractExecutionSource( + dataContainer, + session.getExecutionContext(), + WebSQLProcessor.this, + sqlQuery); + + try (DBCStatement dbStat = DBUtils.makeStatement( + source, + session, + DBCStatementType.SCRIPT, + sqlQuery, + webDataFilter.getOffset(), + webDataFilter.getLimit())) + { + SqlOutputLogReaderJob sqlOutputLogReaderJob = null; + if (readLogs) { + DBPDataSource dataSource = context.getDataSource(); + DBCServerOutputReader dbcServerOutputReader = DBUtils.getAdapter(DBCServerOutputReader.class, dataSource); + if (dbcServerOutputReader == null) { + dbcServerOutputReader = new DefaultServerOutputReader(); + } + sqlOutputLogReaderJob = new SqlOutputLogReaderJob( + webSession, context, dbStat, dbcServerOutputReader, contextInfo.getId()); + sqlOutputLogReaderJob.schedule(); } - sqlOutputLogReaderJob = new SqlOutputLogReaderJob( - webSession, context, dbStat, dbcServerOutputReader, contextInfo.getId()); - sqlOutputLogReaderJob.schedule(); - } - // Set query timeout - int queryTimeout = (int) session.getDataSource().getContainer().getPreferenceStore() - .getDouble(WebSQLConstants.QUOTA_PROP_SQL_QUERY_TIMEOUT); - if (queryTimeout <= 0) { - queryTimeout = CommonUtils.toInt( - getWebSession().getApplication().getAppConfiguration() - .getResourceQuota(WebSQLConstants.QUOTA_PROP_SQL_QUERY_TIMEOUT)); - } - if (queryTimeout > 0) { - try { - dbStat.setStatementTimeout(queryTimeout); - } catch (Throwable e) { - log.debug("Can't set statement timeout:" + e.getMessage()); + // Set query timeout + int queryTimeout = (int) session.getDataSource().getContainer().getPreferenceStore() + .getDouble(WebSQLConstants.QUOTA_PROP_SQL_QUERY_TIMEOUT); + if (queryTimeout <= 0) { + queryTimeout = CommonUtils.toInt( + getWebSession().getApplication().getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_SQL_QUERY_TIMEOUT)); + } + if (queryTimeout > 0) { + try { + dbStat.setStatementTimeout(queryTimeout); + } catch (Throwable e) { + log.debug("Can't set statement timeout:" + e.getMessage()); + } } - } - boolean hasResultSet = dbStat.executeStatement(); + boolean hasResultSet = dbStat.executeStatement(); - // Wait SqlLogStateJob, if its starts - if (sqlOutputLogReaderJob != null) { - sqlOutputLogReaderJob.join(); + // Wait SqlLogStateJob, if its starts + if (sqlOutputLogReaderJob != null) { + sqlOutputLogReaderJob.join(); + } + fillQueryResults(contextInfo, dataContainer, dbStat, hasResultSet, executeInfo, webDataFilter, dataFilter, dataFormat); + } catch (DBException e) { + throw new InvocationTargetException(e); } - fillQueryResults(contextInfo, dataContainer, dbStat, hasResultSet, executeInfo, webDataFilter, dataFilter, dataFormat); - } catch (DBException e) { - throw new InvocationTargetException(e); } - } - }); + }); + } } catch (DBException e) { throw new DBWebException("Error executing query", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java index c0e1c39136..0c86d13d4d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java @@ -104,4 +104,9 @@ public DBCExecutionContext getExecutionContext() { return DBUtils.getDefaultContext(dataSource, false); } + @NotNull + public SQLScriptContext getScriptContext() { + return queryDataContainer.getScriptContext(); + } + } From 6c609d7f62337463a66a34e76dbcb57cb2190472 Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:51:00 +0200 Subject: [PATCH 18/18] CB-4139 update clickhouse driver (#2070) Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- server/drivers/clickhouse_com/pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/drivers/clickhouse_com/pom.xml b/server/drivers/clickhouse_com/pom.xml index c3d83b333d..62b70a3708 100644 --- a/server/drivers/clickhouse_com/pom.xml +++ b/server/drivers/clickhouse_com/pom.xml @@ -18,7 +18,8 @@ com.clickhouse clickhouse-jdbc - 0.3.2-patch7 + 0.4.6 + all